WHAT - 通过 shadcn 组件源码学习 React

发布于:2024-07-23 ⋅ 阅读:(147) ⋅ 点赞:(0)

一、button

组件来源:https://ui.shadcn.com/docs/components/button

import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const buttonVariants = cva(
    "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
    {
        variants: {
            variant: {
                default:
                    "bg-primary text-primary-foreground hover:bg-primary/90",
                destructive:
                    "bg-destructive text-destructive-foreground hover:bg-destructive/90",
                outline:
                    "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
                secondary:
                    "bg-secondary text-secondary-foreground hover:bg-secondary/80",
                disabled:
                    "disabled-foreground bg-disabled text-disabled-foreground",
                ghost: "hover:bg-accent focus-visible:ring-0 focus-visible:ring-offset-0",
                link: "hover:text-primary underline-offset-4",
                icon: "border border-input",
            },
            size: {
                default: "h-8 px-5 py-1.5",
                sm: "h-9 rounded-md px-3",
                lg: "h-11 rounded-md px-8",
                icon: "h-6 w-6",
                iconSm: "h-8 w-8",
                ssm: "h-6",
            },
        },
        defaultVariants: {
            variant: "default",
            size: "default",
        },
    },
)

export interface ButtonProps
    extends React.ButtonHTMLAttributes<HTMLButtonElement>,
        VariantProps<typeof buttonVariants> {
    asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
    ({ className, variant, size, asChild = false, ...props }, ref) => {
        const Comp = asChild ? Slot : "button"
        return (
            <Comp
                className={cn(buttonVariants({ variant, size, className }))}
                ref={ref}
                {...props}
            />
        )
    },
)
Button.displayName = "Button"

export { Button, buttonVariants }

这段代码定义了一个可变样式的按钮组件Button,使用了多个工具和库。我们将逐步解释各部分代码的作用。

1. 导入部分

import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
  • React: 导入React库。
  • Slot: 从@radix-ui/react-slot库中导入Slot组件,用于支持“asChild”属性。
  • cvaVariantProps: 从class-variance-authority库中导入,用于定义可变样式。
  • cn: 从项目中的utils工具库导入cn函数,用于合并CSS类名。

2. 定义按钮的样式变体

const buttonVariants = cva(
    "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
    {
        variants: {
            variant: {
                default: "bg-primary text-primary-foreground hover:bg-primary/90",
                destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
                outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
                secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
                disabled: "disabled-foreground bg-disabled text-disabled-foreground",
                ghost: "hover:bg-accent focus-visible:ring-0 focus-visible:ring-offset-0",
                link: "hover:text-primary underline-offset-4",
                icon: "border border-input",
            },
            size: {
                default: "h-8 px-5 py-1.5",
                sm: "h-9 rounded-md px-3",
                lg: "h-11 rounded-md px-8",
                icon: "h-6 w-6",
                iconSm: "h-8 w-8",
                ssm: "h-6",
            },
        },
        defaultVariants: {
            variant: "default",
            size: "default",
        },
    },
)
  • buttonVariants: 使用cva函数定义了按钮的样式变体。这个对象包含了两个主要部分:
    • variants: 定义了不同的变体选项,如variantsize,每个选项又包含不同的具体样式。
    • defaultVariants: 定义了默认的变体值。

当然,让我们逐一解释variants对象中的每个属性及其对应的CSS属性值。

1. variant

variant属性定义了按钮的多种视觉风格,每种风格都对应一组CSS类名。

  • default

    bg-primary {
      background-color: var(--primary-color);
    }
    text-primary-foreground {
      color: var(--primary-foreground-color);
    }
    hover:bg-primary/90 {
      background-color: var(--primary-color);
      opacity: 0.9;
    }
    
  • destructive

    bg-destructive {
      background-color: var(--destructive-color);
    }
    text-destructive-foreground {
      color: var(--destructive-foreground-color);
    }
    hover:bg-destructive/90 {
      background-color: var(--destructive-color);
      opacity: 0.9;
    }
    
  • outline

    border border-input {
      border: 1px solid var(--input-border-color);
    }
    bg-background {
      background-color: var(--background-color);
    }
    hover:bg-accent {
      background-color: var(--accent-color);
    }
    hover:text-accent-foreground {
      color: var(--accent-foreground-color);
    }
    
  • secondary

    bg-secondary {
      background-color: var(--secondary-color);
    }
    text-secondary-foreground {
      color: var(--secondary-foreground-color);
    }
    hover:bg-secondary/80 {
      background-color: var(--secondary-color);
      opacity: 0.8;
    }
    
  • disabled

    disabled-foreground {
      color: var(--disabled-foreground-color);
    }
    bg-disabled {
      background-color: var(--disabled-color);
    }
    text-disabled-foreground {
      color: var(--disabled-foreground-color);
    }
    
  • ghost

    hover:bg-accent {
      background-color: var(--accent-color);
    }
    focus-visible:ring-0 {
      outline: none;
      box-shadow: none;
    }
    focus-visible:ring-offset-0 {
      box-shadow: none;
    }
    
  • link

    hover:text-primary {
      color: var(--primary-color);
    }
    underline-offset-4 {
      text-underline-offset: 4px;
    }
    
  • icon

    border border-input {
      border: 1px solid var(--input-border-color);
    }
    

2. size

size属性定义了按钮的不同尺寸,每个尺寸都对应一组CSS类名。

  • default

    h-8 {
      height: 2rem; /* 32px */
    }
    px-5 {
      padding-left: 1.25rem; /* 20px */
      padding-right: 1.25rem; /* 20px */
    }
    py-1.5 {
      padding-top: 0.375rem; /* 6px */
      padding-bottom: 0.375rem; /* 6px */
    }
    
  • sm

    h-9 {
      height: 2.25rem; /* 36px */
    }
    rounded-md {
      border-radius: 0.375rem; /* 6px */
    }
    px-3 {
      padding-left: 0.75rem; /* 12px */
      padding-right: 0.75rem; /* 12px */
    }
    
  • lg

    h-11 {
      height: 2.75rem; /* 44px */
    }
    rounded-md {
      border-radius: 0.375rem; /* 6px */
    }
    px-8 {
      padding-left: 2rem; /* 32px */
      padding-right: 2rem; /* 32px */
    }
    
  • icon

    h-6 {
      height: 1.5rem; /* 24px */
    }
    w-6 {
      width: 1.5rem; /* 24px */
    }
    
  • iconSm

    h-8 {
      height: 2rem; /* 32px */
    }
    w-8 {
      width: 2rem; /* 32px */
    }
    
  • ssm

    h-6 {
      height: 1.5rem; /* 24px */
    }
    

总结

这些variants属性提供了丰富的样式变体,使得按钮组件可以根据不同的需求应用不同的外观和尺寸,通过简单的属性传递实现了多样化的视觉效果。

3. 定义按钮的属性类型

export interface ButtonProps
    extends React.ButtonHTMLAttributes<HTMLButtonElement>,
        VariantProps<typeof buttonVariants> {
    asChild?: boolean
}
  • ButtonProps: 定义了按钮组件的属性接口,扩展了React.ButtonHTMLAttributesVariantProps。另外,还增加了asChild属性,用于指定是否将按钮作为子组件渲染。

4. 定义按钮组件

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
    ({ className, variant, size, asChild = false, ...props }, ref) => {
        const Comp = asChild ? Slot : "button"
        return (
            <Comp
                className={cn(buttonVariants({ variant, size, className }))}
                ref={ref}
                {...props}
            />
        )
    },
)
Button.displayName = "Button"
  • Button: 使用React.forwardRef定义一个带有转发引用(ref)的按钮组件。
    • Comp: 根据asChild属性,动态决定使用Slot组件还是button元素。
    • cn(buttonVariants({ variant, size, className })): 使用cn函数合并传入的className和通过buttonVariants生成的变体样式。

5. 导出组件和样式变体

export { Button, buttonVariants }
  • ButtonbuttonVariants均被导出,允许在其他模块中使用。

总结

这个组件使用了class-variance-authority库来管理按钮的样式变体,通过React的forwardRef和条件渲染实现了灵活的按钮组件。这样,开发者可以通过简单的属性传递来改变按钮的外观和行为。

二、multi-select

组件来源:https://shadcn-extension.vercel.app/docs/multi-select

如果希望有远程搜索能力,可以参考 https://shadcnui-expansions.typeart.cc/docs/multiple-selector

"use client";

import { Badge } from "@/components/ui/badge";
import {
  Command,
  CommandItem,
  CommandEmpty,
  CommandList,
} from "@/components/ui/command";
import { cn } from "@/lib/utils";
import { Command as CommandPrimitive } from "cmdk";
import { X as RemoveIcon, Check } from "lucide-react";
import React, {
  KeyboardEvent,
  createContext,
  forwardRef,
  useCallback,
  useContext,
  useState,
} from "react";

type MultiSelectorProps = {
  values: string[];
  onValuesChange: (value: string[]) => void;
  loop?: boolean;
  options: {label: string, value: string, color?: string}[];
} & React.ComponentPropsWithoutRef<typeof CommandPrimitive>;

interface MultiSelectContextProps {
  value: string[];
  options: {label: string, value: string, color?: string}[];
  onValueChange: (value: any) => void;
  open: boolean;
  setOpen: (value: boolean) => void;
  inputValue: string;
  setInputValue: React.Dispatch<React.SetStateAction<string>>;
  activeIndex: number;
  setActiveIndex: React.Dispatch<React.SetStateAction<number>>;
}

const MultiSelectContext = createContext<MultiSelectContextProps | null>(null);

const useMultiSelect = () => {
  const context = useContext(MultiSelectContext);
  if (!context) {
    throw new Error("useMultiSelect must be used within MultiSelectProvider");
  }
  return context;
};

const MultiSelector = ({
  values: value,
  onValuesChange: onValueChange,
  loop = false,
  className,
  children,
  dir,
  options,
  ...props
}: MultiSelectorProps) => {
  const [inputValue, setInputValue] = useState("");
  const [open, setOpen] = useState<boolean>(false);
  const [activeIndex, setActiveIndex] = useState<number>(-1);

  const onValueChangeHandler = useCallback(
    (val: string) => {
      if (value.includes(val)) {
        onValueChange(value.filter((item) => item !== val));
      } else {
        onValueChange([...value, val]);
      }
    },
    [value]
  );

  // TODO : change from else if use to switch case statement

  const handleKeyDown = useCallback(
    (e: KeyboardEvent<HTMLDivElement>) => {
      const moveNext = () => {
        const nextIndex = activeIndex + 1;
        setActiveIndex(
          nextIndex > value.length - 1 ? (loop ? 0 : -1) : nextIndex
        );
      };

      const movePrev = () => {
        const prevIndex = activeIndex - 1;
        setActiveIndex(prevIndex < 0 ? value.length - 1 : prevIndex);
      };

      if ((e.key === "Backspace" || e.key === "Delete") && value.length > 0) {
        if (inputValue.length === 0) {
          if (activeIndex !== -1 && activeIndex < value.length) {
            onValueChange(value.filter((item) => item !== value[activeIndex]));
            const newIndex = activeIndex - 1 < 0 ? 0 : activeIndex - 1;
            setActiveIndex(newIndex);
          } else {
            onValueChange(
              value.filter((item) => item !== value[value.length - 1])
            );
          }
        }
      } else if (e.key === "Enter") {
        setOpen(true);
      } else if (e.key === "Escape") {
        if (activeIndex !== -1) {
          setActiveIndex(-1);
        } else {
          setOpen(false);
        }
      } else if (dir === "rtl") {
        if (e.key === "ArrowRight") {
          movePrev();
        } else if (e.key === "ArrowLeft" && (activeIndex !== -1 || loop)) {
          moveNext();
        }
      } else {
        if (e.key === "ArrowLeft") {
          movePrev();
        } else if (e.key === "ArrowRight" && (activeIndex !== -1 || loop)) {
          moveNext();
        }
      }
    },
    [value, inputValue, activeIndex, loop]
  );

  return (
    <MultiSelectContext.Provider
      value={{
        value,
        onValueChange: onValueChangeHandler,
        open,
        setOpen,
        inputValue,
        setInputValue,
        activeIndex,
        setActiveIndex,
        options,
      }}
    >
      <Command
        onKeyDown={handleKeyDown}
        className={cn(
          "overflow-visible bg-transparent flex flex-col",
          className
        )}
        dir={dir}
        {...props}
      >
        {children}
      </Command>
    </MultiSelectContext.Provider>
  );
};

const MultiSelectorTrigger = forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, children, ...props }, ref) => {
  const { value, onValueChange, activeIndex, open, options } = useMultiSelect();

  const mousePreventDefault = useCallback((e: React.MouseEvent) => {
    e.preventDefault();
    e.stopPropagation();
  }, []);

  const valueOptions = options.filter(option => value.includes(option.value))

  return (
    <div
      ref={ref}
      className={cn(
        "min-h-9 bg-accent text-sm flex items-center flex-wrap gap-1 px-3 rounded-lg mb-2",
        open ? "ring-ring ring-1 ring-offset-1 bg-background" : "",
        className
      )}
      {...props}
    >
      {valueOptions.map((item, index) => (
        <Badge
          key={item.value}
          color={item.color}
          className={cn(
            "flex flex-wrap gap-1",
            activeIndex === index && "ring-2 ring-muted-foreground"
          )}
        >
          <span>{item.label}</span>
          <button
            aria-label={`Remove ${item.label} option`}
            aria-roledescription="button to remove option"
            type="button"
            onMouseDown={mousePreventDefault}
            onClick={() => onValueChange(item.value)}
          >
            <span className="sr-only">Remove {item.label} option</span>
            <RemoveIcon className="h-4 w-4 hover:stroke-destructive" />
          </button>
        </Badge>
      ))}
      {children}
    </div>
  );
});

MultiSelectorTrigger.displayName = "MultiSelectorTrigger";

const MultiSelectorInput = forwardRef<
  React.ElementRef<typeof CommandPrimitive.Input>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => {
  const { setOpen, inputValue, setInputValue, activeIndex, setActiveIndex } =
    useMultiSelect();
  return (
    <CommandPrimitive.Input
      {...props}
      ref={ref}
      value={inputValue}
      onValueChange={activeIndex === -1 ? setInputValue : undefined}
      onBlur={() => setOpen(false)}
      onFocus={() => setOpen(true)}
      onClick={() => setActiveIndex(-1)}
      className={cn(
        "bg-transparent outline-none placeholder:text-muted-foreground flex-1",
        className,
        activeIndex !== -1 && "caret-transparent"
      )}
    />
  );
});

MultiSelectorInput.displayName = "MultiSelectorInput";

const MultiSelectorContent = forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ children }, ref) => {
  const { open } = useMultiSelect();
  return (
    <div ref={ref} className="relative">
      {open && children}
    </div>
  );
});

MultiSelectorContent.displayName = "MultiSelectorContent";

const MultiSelectorList = forwardRef<
  React.ElementRef<typeof CommandPrimitive.List>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, children }, ref) => {
  return (
    <CommandList
      ref={ref}
      className={cn(
        "p-2 flex flex-col gap-2 rounded-md scrollbar-thin scrollbar-track-transparent transition-colors scrollbar-thumb-muted-foreground dark:scrollbar-thumb-muted scrollbar-thumb-rounded-lg w-full absolute bg-background shadow-md z-10 border border-muted top-0",
        className
      )}
    >
      {children}
      <CommandEmpty>
        <span className="text-muted-foreground">No results found</span>
      </CommandEmpty>
    </CommandList>
  );
});

MultiSelectorList.displayName = "MultiSelectorList";

const MultiSelectorItem = forwardRef<
  React.ElementRef<typeof CommandPrimitive.Item>,
  { value: string } & React.ComponentPropsWithoutRef<
    typeof CommandPrimitive.Item
  >
>(({ className, value, children, ...props }, ref) => {
  const { value: Options, onValueChange, setInputValue } = useMultiSelect();

  const mousePreventDefault = useCallback((e: React.MouseEvent) => {
    e.preventDefault();
    e.stopPropagation();
  }, []);

  const isIncluded = Options.includes(value);
  return (
    <CommandItem
      ref={ref}
      {...props}
      onSelect={() => {
        onValueChange(value);
        setInputValue("");
      }}
      className={cn(
        "rounded-md cursor-pointer px-4 py-1.5 transition-colors flex justify-between ",
        className,
        isIncluded && "opacity-50 cursor-default",
        props.disabled && "opacity-50 cursor-not-allowed"
      )}
      onMouseDown={mousePreventDefault}
    >
      {children}
      {isIncluded && <Check className="h-4 w-4" />}
    </CommandItem>
  );
});

MultiSelectorItem.displayName = "MultiSelectorItem";

export {
  MultiSelector,
  MultiSelectorTrigger,
  MultiSelectorInput,
  MultiSelectorContent,
  MultiSelectorList,
  MultiSelectorItem,
};

这个组件库实现了一个多选下拉框,包含选择、显示和过滤选项等功能。组件利用了 React 的上下文、钩子和基于 Radix UI 和 CMDK 的组合控件。

以下是对每个主要部分的详细分析:

多选组件的核心上下文与状态

1. 上下文与钩子

const MultiSelectContext = createContext<MultiSelectContextProps | null>(null);

const useMultiSelect = () => {
  const context = useContext(MultiSelectContext);
  if (!context) {
    throw new Error("useMultiSelect must be used within MultiSelectProvider");
  }
  return context;
};

MultiSelectContext 是一个 React 上下文,用于共享多选组件的状态。useMultiSelect 是一个自定义钩子,用于方便地访问这个上下文。

2. MultiSelector 组件

const MultiSelector = ({
  values: value,
  onValuesChange: onValueChange,
  loop = false,
  className,
  children,
  dir,
  options,
  ...props
}: MultiSelectorProps) => {
  const [inputValue, setInputValue] = useState("");
  const [open, setOpen] = useState<boolean>(false);
  const [activeIndex, setActiveIndex] = useState<number>(-1);

  const onValueChangeHandler = useCallback(
    (val: string) => {
      if (value.includes(val)) {
        onValueChange(value.filter((item) => item !== val));
      } else {
        onValueChange([...value, val]);
      }
    },
    [value]
  );
  
  const handleKeyDown = useCallback(
    (e: KeyboardEvent<HTMLDivElement>) => {
      // handle keyboard navigation and actions
    },
    [value, inputValue, activeIndex, loop]
  );

  return (
    <MultiSelectContext.Provider
      value={{
        value,
        onValueChange: onValueChangeHandler,
        open,
        setOpen,
        inputValue,
        setInputValue,
        activeIndex,
        setActiveIndex,
        options,
      }}
    >
      <Command
        onKeyDown={handleKeyDown}
        className={cn(
          "overflow-visible bg-transparent flex flex-col",
          className
        )}
        dir={dir}
        {...props}
      >
        {children}
      </Command>
    </MultiSelectContext.Provider>
  );
};

MultiSelector 是整个多选组件的核心,负责管理状态并提供上下文。它使用 useState 管理输入值、打开状态和活动索引。通过 useCallback 创建 onValueChangeHandlerhandleKeyDown 函数,用于处理选项的选择和键盘事件。

组件子部分

1. MultiSelectorTrigger

const MultiSelectorTrigger = forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, children, ...props }, ref) => {
  const { value, onValueChange, activeIndex, open, options } = useMultiSelect();

  const mousePreventDefault = useCallback((e: React.MouseEvent) => {
    e.preventDefault();
    e.stopPropagation();
  }, []);

  const valueOptions = options.filter(option => value.includes(option.value))

  return (
    <div
      ref={ref}
      className={cn(
        "min-h-9 bg-accent text-sm flex items-center flex-wrap gap-1 px-3 rounded-lg mb-2",
        open ? "ring-ring ring-1 ring-offset-1 bg-background" : "",
        className
      )}
      {...props}
    >
      {valueOptions.map((item, index) => (
        <Badge
          key={item.value}
          color={item.color}
          className={cn(
            "flex flex-wrap gap-1",
            activeIndex === index && "ring-2 ring-muted-foreground"
          )}
        >
          <span>{item.label}</span>
          <button
            aria-label={`Remove ${item.label} option`}
            aria-roledescription="button to remove option"
            type="button"
            onMouseDown={mousePreventDefault}
            onClick={() => onValueChange(item.value)}
          >
            <span className="sr-only">Remove {item.label} option</span>
            <RemoveIcon className="h-4 w-4 hover:stroke-destructive" />
          </button>
        </Badge>
      ))}
      {children}
    </div>
  );
});

MultiSelectorTrigger.displayName = "MultiSelectorTrigger";

MultiSelectorTrigger 是一个用于显示已选择选项的组件。它使用 useMultiSelect 钩子从上下文获取状态,并渲染已选项的 Badge 组件。每个 Badge 组件包含一个按钮,用于移除该选项。

2. MultiSelectorInput

const MultiSelectorInput = forwardRef<
  React.ElementRef<typeof CommandPrimitive.Input>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => {
  const { setOpen, inputValue, setInputValue, activeIndex, setActiveIndex } =
    useMultiSelect();
  return (
    <CommandPrimitive.Input
      {...props}
      ref={ref}
      value={inputValue}
      onValueChange={activeIndex === -1 ? setInputValue : undefined}
      onBlur={() => setOpen(false)}
      onFocus={() => setOpen(true)}
      onClick={() => setActiveIndex(-1)}
      className={cn(
        "bg-transparent outline-none placeholder:text-muted-foreground flex-1",
        className,
        activeIndex !== -1 && "caret-transparent"
      )}
    />
  );
});

MultiSelectorInput.displayName = "MultiSelectorInput";

MultiSelectorInput 是一个输入组件,用于处理用户输入。它使用 useMultiSelect 钩子从上下文获取状态,并根据输入值更新上下文中的 inputValue。它还处理输入框的焦点和点击事件。

3. MultiSelectorContent

const MultiSelectorContent = forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ children }, ref) => {
  const { open } = useMultiSelect();
  return (
    <div ref={ref} className="relative">
      {open && children}
    </div>
  );
});

MultiSelectorContent.displayName = "MultiSelectorContent";

MultiSelectorContent 是一个包装组件,用于渲染多选内容。当上下文中的 open 状态为 true 时,显示其子组件。

4. MultiSelectorList

const MultiSelectorList = forwardRef<
  React.ElementRef<typeof CommandPrimitive.List>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, children }, ref) => {
  return (
    <CommandList
      ref={ref}
      className={cn(
        "p-2 flex flex-col gap-2 rounded-md scrollbar-thin scrollbar-track-transparent transition-colors scrollbar-thumb-muted-foreground dark:scrollbar-thumb-muted scrollbar-thumb-rounded-lg w-full absolute bg-background shadow-md z-10 border border-muted top-0",
        className
      )}
    >
      {children}
      <CommandEmpty>
        <span className="text-muted-foreground">No results found</span>
      </CommandEmpty>
    </CommandList>
  );
});

MultiSelectorList.displayName = "MultiSelectorList";

MultiSelectorList 是一个列表组件,用于渲染所有可选项。它使用 CommandList 组件,并在子组件中包含一个 CommandEmpty 组件,当没有结果时显示提示。

5. MultiSelectorItem

const MultiSelectorItem = forwardRef<
  React.ElementRef<typeof CommandPrimitive.Item>,
  { value: string } & React.ComponentPropsWithoutRef<
    typeof CommandPrimitive.Item
  >
>(({ className, value, children, ...props }, ref) => {
  const { value: Options, onValueChange, setInputValue } = useMultiSelect();

  const mousePreventDefault = useCallback((e: React.MouseEvent) => {
    e.preventDefault();
    e.stopPropagation();
  }, []);

  const isIncluded = Options.includes(value);
  return (
    <CommandItem
      ref={ref}
      {...props}
      onSelect={() => {
        onValueChange(value);
        setInputValue("");
      }}
      className={cn(
        "rounded-md cursor-pointer px-4 py-1.5 transition-colors flex justify-between ",
        className,
        isIncluded && "opacity-50 cursor-default",
        props.disabled && "opacity-50 cursor-not-allowed"
      )}
      onMouseDown={mousePreventDefault}
    >
      {children}
      {isIncluded && <Check className="h-4 w-4" />}
    </CommandItem>
  );
});

MultiSelectorItem.displayName = "MultiSelectorItem";

MultiSelectorItem 是一个可选项组件。它使用 useMultiSelect 钩子从上下文获取状态,并在选中时调用 onValueChange 更新上下文中的选项状态。它还会在已选项时显示一个 Check 图标。

总结

这个多选组件库通过上下文和钩子共享状态,并将组件划分为多个小组件,每个小组件负责处理不同的功能和渲染部分。


网站公告

今日签到

点亮在社区的每一天
去签到