原文链接https://www.castamerego.com/blog/Link

一个超链接小组件,请点击原文查看效果

缘起

最近想制作一个参考文献的小组件,用于在文末显示所有的参考文献。过程中又想搞一个好看点的超链接小组件,于是就有了本篇

实现

基础功能

基础功能其实很简单,我们只需要传入一个标题和一个链接,然后渲染一个<a>标签即可

基础功能

import React from "react";

interface Props {
  title: string;
  url: string;
}

const Link = ({ title, url }: Props) => {
  return (
    <a href={url} target="_blank" rel="noopener noreferrer">
      {title}
    </a>
  );
};

export default Link;

点击参数

这一部分是修改链接点击时的操作,比如是否打开一个新的标签页,以及当链接和当前 url 相同时的操作等

信息

Docusaurus 中获取当前 url 的方法可参考这里

我们需要处理的是 <a> 链接的两个参数,href 和 target,逻辑如下

效果如下:

  1. 打开新页面跳转到主页

    超链接小组件 <Link2 title="超链接小组件" url="/" />

  2. 在当前页面跳转到主页

    超链接小组件 <Link2 title="超链接小组件" url="/" open="sameTab" />

  3. 在新页面打开本页

    超链接小组件 <Link2 title="超链接小组件" url="/blog/Link" onSameUrl="open" />

  4. 关闭链接

    超链接小组件 <Link2 title="超链接小组件" url="/blog/Link" onSameUrl="disable" />

  5. 刷新本页

    超链接小组件 <Link2 title="超链接小组件" url="/blog/Link" open="sameTab" />

点击参数

import { useLocation } from "@docusaurus/router";
import React from "react";

interface Props {
  title: string;
  url: string;
  open?: "newTab" | "sameTab";
  onSameUrl?: "open" | "refresh" | "disable";
}

const Link = ({
  title,
  url,
  open = "newTab",
  onSameUrl = "refresh",
}: Props) => {
  const isSameUrl = useLocation().pathname === url;

  return (
    <a
      href={isSameUrl ? (onSameUrl === "disable" ? undefined : url) : url}
      target={
        open === "newTab"
          ? isSameUrl
            ? onSameUrl === "open"
              ? "_blank"
              : undefined
            : "_blank"
          : undefined
      }
      rel="noopener noreferrer"
    >
      {title}
    </a>
  );
};

export default Link;

样式

important

若想在你的 docusaurus 网站中使用下面的样式,请先参考这里,配置好如下内容

  • tailwindcss
  • classNames

备注

由于只演示样式,笔者在下面都关闭了链接的跳转功能(即设置为onSameUrl="disable")

颜色

笔者使用了 tailwindcss 的颜色来进行演示。这里使用了 useColorMode 钩子来获取当前主题颜色,然后通过 classNames 来动态设置颜色,效果如下。会随着主题颜色的变化而变化,并且会在 hover 时改变颜色

超链接小组件 Hover Me!

  import { useLocation } from "@docusaurus/router";
+ import { useColorMode } from "@docusaurus/theme-common";
+ import classNames from "classnames";
  import React from "react";

  interface Props {
    title: string;
    url: string;
    open?: "newTab" | "sameTab";
    onSameUrl?: "open" | "refresh" | "disable";
  }
+ const colors = [
+   {
+     main: "text-blue-500",
+     hover: "hover:text-blue-600",
+   },
+   {
+     main: "text-gray-500",
+     hover: "hover:text-gray-400",
+   },
+ ];

  const Link = ({
    title,
    url,
    open = "newTab",
    onSameUrl = "refresh",
  }: Props) => {
+   const color = colors[useColorMode().colorMode === "dark" ? 1 : 0];
    const isSameUrl = useLocation().pathname === url;

    return (
      <a
+       className={classNames({
+         "relative inline-block transition-all duration-300 ": true,
+         [color.main]: true,
+         [color.hover]: true,
+       })}
        href={isSameUrl ? (onSameUrl === "disable" ? undefined : url) : url}
        target={
          open === "newTab"
            ? isSameUrl
              ? onSameUrl === "open"
                ? "_blank"
                : undefined
              : "_blank"
            : undefined
        }
        rel="noopener noreferrer"
      >
        {title}
      </a>
    );
  };

  export default Link;

动态下划线

接下来是实现一个动态下划线的效果,效果如下。其特点为,当鼠标进入时,下划线会从左向右画出,而当鼠标离开时,下划线会从右向左擦除

link4

接下来就来一步步讲如何实现

1. 添加一个下划线

这是一个超链接

这一步其实还算比较简单,在之前的组件中,显示 title 的下方加一个 span,添加如下样式即可

下划线

  ...
  const colors = [
    {
      main: "text-blue-500",
      hover: "hover:text-blue-700",
+     underScore: "bg-blue-400",
    },
    {
      main: "text-gray-500",
      hover: "hover:text-gray-400",
+     underScore: "bg-gray-300",
    },
  ];

  const Link = ({
    ...
  }: Props) => {
    ...

    return (
      <a
        ...
      >
        {title}
+       <span
+         className={classNames({
+           "absolute left-0 bottom-0 h-0.5": true,
+           "w-full": true,
+           [color.underScore]: true,
+         })}
+       />
      </a>
    );
  };

在这里就体现出了使用 classNames 这个库的好处,可以更条例的写样式,并且可以很方便的条件渲染样式,在接下来的步骤更会体现出他的优点

2. 添加 hover 动画

接下来就是添加在 hover 时的动画效果

这是一个超链接 Hover Me!

我们首先添加一个 hover 的状态,当鼠标进入时,将 hover 设置为 true,当鼠标离开时,将 hover 设置为 false

然后根据 hover 的值来决定下划线的宽度,当 hover 为 true 时,下划线的宽度为 100%,当 hover 为 false 时,下划线的宽度为 0,再添加一个 transition-all 的过渡动画,就可以实现 hover 动画了

  ...
  const Link = ({
    ...
  }: Props) => {
+   const [hover, setHover] = useState(false);
    const color = colors[useColorMode().colorMode === "dark" ? 1 : 0];
    const isSameUrl = useLocation().pathname === url;

    return (
      <a
        ...
+       onMouseEnter={() => setHover(true)}
+       onMouseLeave={() => setHover(false)}
      >
        {title}
        <span
          className={classNames({
-           "absolute left-0 bottom-0 h-0.5": true,
+           "absolute left-0 bottom-0 h-0.5 transition-all duration-500": true,
-           "w-full": true,
+           "w-full": hover,
+           "w-0": !hover,
            [color.underScore]: true,
          })}
        />
      </a>
    );
  };

但这个效果并不是笔者想要的,笔者想要的是当鼠标离开时,下划线可以从右向左擦除

3. 添加擦除动画

我们可以发现,上一步的擦除是从左向右,那我们直接将左对齐改成右对齐试一下。为了方便演示,笔者将两个下划线都添加上去,看一下效果

这是一个超链接 Hover Me!

看着似乎不错,我们只要在进入 hover 时,设置为左对齐,离开时,设置为右对齐即可,让我们试一下

这是一个超链接 Hover Me!

  <span
    className={classNames({
-     "absolute left-0 bottom-0 h-0.5 transition-all duration-500": true,
+     "absolute bottom-0 h-0.5 transition-all duration-500": true,
-     "w-full": hover,
+     "w-full left-0": hover,
-     "w-0": !hover,
+     "w-0 right-0": !hover,
      [color.underScore]: true,
    })}
  />

4. 优化

到目前已经看着很完美了,但还没达到笔者的预期。读者们可以试一下以下两组超链接的区别,试着用鼠标快速扫过所有链接

这是一个超链接 Hover Me!
这是一个超链接 Hover Me!
这是一个超链接 Hover Me!
这是一个超链接 Hover Me!

这是一个超链接 Hover Me!
这是一个超链接 Hover Me!
这是一个超链接 Hover Me!
这是一个超链接 Hover Me!

效果应该如下:

link compare

笔者想要的是下面这种效果,即不管是鼠标快速扫过还是悬浮不动,下划线都会先全拉满,再进行下一步的操作。而上一步中的动画,仅仅是检测鼠标是否 hover,当离开 hover 的一瞬间,就会切换到擦除,这样会有一种动画未完成的割裂感

这是一个超链接 Hover Me!

其实现方式如下。首先,在 onMouseEnter 中,我们设置 hover 为 true,同时设置一个 500ms(笔者的 span 动画为 500ms) 的定时器。在 onMouseLeave 中,我们先判断 timeoutId 是否存在,如果存在,则说明刚刚的定时器还没跑完(即进入的动画还没完成),此时我们就等个 300ms,再设置 hover 为 false。如果 timeoutId 不存在,则说明刚刚的定时器已经跑完,此时我们直接设置 hover 为 false 即可。代码如下

- import React, { useRef } from "react";
+ import React, { useRef, useState } from "react";

  const Link = ({...}: Props) => {
    const [hover, setHover] = useState(false);
    const color = colors[useColorMode().colorMode === "dark" ? 1 : 0];
    const isSameUrl = useLocation().pathname === url;
+   const timeoutId = useRef<number | null>(null);

    return (
      <a
        ...
        onMouseEnter={() => {
          setHover(true);
+         if (timeoutId.current) {
+           clearTimeout(timeoutId.current);
+         }
+         timeoutId.current = window.setTimeout(() => {
+           timeoutId.current = null;
          }, 500);
        }}
        onMouseLeave={() => {
+         if (timeoutId.current) {
+           clearTimeout(timeoutId.current);
+           timeoutId.current = window.setTimeout(() => setHover(false), 300);
+         } else setHover(false);
        }}
      >
      ...
      </a>
    );
  };

5. 更有甚者

其实到上一步已经差不多了,但无奈笔者有点强迫症,又发现了一点不太舒服的地方。读者可以再试试下面两个超链接,看下划线有什么不同

这是一个超链接 Hover Me!

这是一个超链接 Hover Me!

答案揭晓,上一步做出的效果中,假如用户在下划线擦除的过程中,又进入到了 hover 状态,那么会直接打断施法,直接进入下划线拉满的状态...。而下面优化过的版本是,在这种情况下,会直接开一个新的动画,且对上一个的擦除也没有影响,即可能存在一边擦除,一边在画下划线的情况

旧:

link old

新:

link new

笔者只能说,这是一个很细的东西,其实不去管他也无妨。但这里还是给出笔者的解决方案,下面把动画放慢,并切换一下颜色,相信聪明的读者就能看出来是怎么实现的了

这是一个超链接 Hover Me!

其实就是把原来的一个 <span> 拆成了两个,一个负责画下划线,一个负责擦除下划线。而两个动画的逻辑,才是需要着重考虑的,这里笔者直接给出代码,有兴趣的读者可以自行研究

最终优化

这里有一个比较有意思的点要提一下,通过把 transition-all duration-500 从总是 true,切换到 hover 下,我们就实现了在鼠标进入的时候,会有过渡效果,而在鼠标离开的时候,没有过渡效果(瞬间消失)。而擦除的效果则是反过来,这个使用 classNames 和样式的技巧,在其他地方或许也可以用到

  <span
    className={classNames({
-     "absolute left-0 bottom-0 h-0.5 transition-all duration-500": true,
+     "absolute left-0 bottom-0 h-0.5": true,
-     "w-full ": hover,
+     "w-full transition-all duration-500": hover,
      [color.underScore]: true,
      "w-0": !hover,
    })}
  />

其他

距离

当字号 > 3xl 时,应该让下划线和文字之间有一定的距离,效果如下,手动添加了一个 size 的参数

这是一个 4xl 超链接

这是一个 3xl 超链接

这是一个 2xl 超链接

这是一个 xl 超链接

这是一个 lg 超链接

这是一个 sm 超链接 

原文链接https://www.castamerego.com/blog/Link

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐