制作一个搜索弹窗组件(Tailwind CSS + shadcn/ui)

实现效果

在线演示

如何实现

创建一个项目,并引入 shadcn/ui 组件库

这里我们直接使用 shadcn/ui 中的 Dialog 组件,也可以按照项目所使用的组件库自行选择或者封装。

  1. 创建一个 Next.js 项目
bash 复制代码
npx create-next-app@latest my-app --typescript --tailwind --eslint
  1. 初始化 shadcn/ui 组件库,具体的步骤可以参考文档
bash 复制代码
npx shadcn-ui@latest init
  1. 增加 Dialog, Input 等组件
bash 复制代码
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add input

组件结构

先按照界面实现组件,主要分为触发弹窗的搜索按钮,搜索弹窗组件中的输入框和搜索结果。

按照弹窗组件的使用方式,我们可以将触发弹窗的搜索按钮放在 DialogTrigger 中,将搜索弹窗组件的输入框和搜索结果放在弹窗主要内容内。

tsx 复制代码
import { Button } from '@/components/ui/button';
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogHeader,
  DialogTrigger,
} from '@/components/ui/dialog';
import type { SearchResult } from '@/types';
import { Loader2, SearchIcon } from 'lucide-react';

export default function Search(props: SearchProps) {
  return (
    <Dialog open={open} onOpenChange={handleOpenChange}>
      <DialogTrigger asChild>
        <Button
          variant="secondary"
          className="h-9 justify-start rounded-2xl bg-transparent pl-2 focus-visible:ring-0 focus-visible:ring-offset-0 lg:w-64 lg:bg-[#EBEDF0] lg:hover:bg-[#EBEDF0]/70"
        >
          <SearchIcon className="text-black-1" size={24} />
          <span className="text-gray-1 ml-1 hidden lg:inline">搜索</span>
        </Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w[100vw] top-0 translate-y-0 gap-0 p-0 lg:top-28 lg:max-w-[640px] 2xl:max-w-[720px]">
        <DialogHeader>
          <div className="flex items-center border-b px-4 py-2">
            <SearchIcon size={24} className="text-gray-1" />
            <Input
              ref={inputRef}
              className="text-black-1 placeholder:text-gray-1 !border-none text-xl !shadow-none !outline-none !ring-0"
              placeholder="搜索文档"
              value={value}
              onChange={(e) => {
                onChange(e.target.value);
              }}
            ></Input>
            <DialogClose className="text-primary flex-shrink-0 border-l pl-4 text-xl">
              取消
            </DialogClose>
          </div>
        </DialogHeader>
        <div className="max-h-screen min-h-screen overflow-auto lg:max-h-96 lg:min-h-[198px]">
          <SearchResults
            results={results}
            onClickResult={handleClickResult}
          />
        </div>
      </DialogContent>
    </Dialog>
  );
}

type SearchResultsProps = {
  results?: SearchResult[];
  onClickResult?: () => void;
};

function SearchResults(props: SearchResultsProps) {
  const { results, onClickResult } = props;

  if (!results?.length) {
    return <EmptyResult>查无结果</EmptyResult>;
  }

  return <div className="px-4 pb-16 lg:pb-4">{/* 列表,展示搜索结果 */}</div>;
}

function EmptyResult({ children }: { children: React.ReactNode }) {
  return (
    <div className="text-gray-1 h-full min-h-48 pt-16 text-center text-xl font-medium">
      {children}
    </div>
  );
}

实现交互

搜索需要考虑到页面的交互和实际搜索逻辑的实现,实际实现中有多种方式,比如使用hooks封装逻辑,使用不同组件封装等。

这里我们参考通用的组件设计实践,将 UI 组件和逻辑分开。Search 这个组件只负责 UI 相关的交互和展示,通过受控组件的方式和其他实际实现搜索逻辑的逻辑组件配合使用,这样可以达到组件分工合理、代码复用、支持不同搜索引擎实现等优点。

所以 Search 这个组件的交互逻辑其实很简单,只需要控制弹窗开启关闭,和控制搜索输入框中输入的值的改变触发搜索即可。

tsx 复制代码
type SearchProps = {
  /** 搜索输入框中的值 */
  value: string;
  /** 搜索输入框中的值改变事件,父组件可以通过该事件触发搜索逻辑并且更新搜索结果 */
  onChange: (value: string) => void;
  /** 搜索结果,根据这个 prop 来更新搜索结果展示 */
  results: SearchResult[];
  onActive?: () => void;
  onInActive?: () => void;
};
tsx 复制代码
  const { value, onChange, results, onActive, onInActive } =
    props;

  const [open, setOpen] = useState(false);
  const inputRef = useRef<HTMLInputElement>(null);

  const isSearching = value && Boolean(value);
  
  // 监听键盘,可以通过 / 或者 crtl+k 打开搜索弹窗
  useEffect(() => {
    const INPUTS = ['INPUT', 'SELECT', 'TEXTAREA'];
    const handleKeydown = (e: KeyboardEvent): void => {
      const isEditingContent = (event: KeyboardEvent) => {
        const element = event.target as HTMLElement;
        const tagName = element.tagName;
        return element.isContentEditable || INPUTS.includes(tagName);
      };

      if (
        !isEditingContent(e) &&
        (e.key === '/' ||
          (e.key === 'k' &&
            (e.metaKey /* for Mac */ || /* for non-Mac */ e.ctrlKey)))
      ) {
        setOpen(true);
        e.preventDefault();
      }
    };

    window.addEventListener('keydown', handleKeydown);
    return () => {
      window.removeEventListener('keydown', handleKeydown);
    };
  }, []);

  // 监听弹窗改变事件,通过事件通知给父组件
  const handleOpenChange = (val: boolean) => {
    setOpen(val);
    if (val) {
      onActive?.();
    } else {
      onInActive?.();
    }
  };

这篇文章简单介绍了如何制作一个搜索组件的 UI,实际要实现搜索功能,需要结合其他搜索引擎(如flexsearch)或者搜索接口,会放在其他文章单独讨论。

参考链接

相关推荐
Larcher1 分钟前
新手也能学会,100行代码玩AI LOGO
前端·llm·html
徐子颐13 分钟前
从 Vibe Coding 到 Agent Coding:Cursor 2.0 开启下一代 AI 开发范式
前端
小月鸭26 分钟前
如何理解HTML语义化
前端·html
jump6801 小时前
url输入到网页展示会发生什么?
前端
诸葛韩信1 小时前
我们需要了解的Web Workers
前端
brzhang1 小时前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu1 小时前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花1 小时前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
十二春秋2 小时前
场景模拟:基础路由配置
前端
六月的可乐2 小时前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程