wui
组件

工具栏 Toolbar

为一组紧凑操作提供排列、分组与方向键导航,并复用现有按钮外观。

第三方依赖 · radix-ui第三方依赖 · class-variance-authority

基础用法

Loading…
pnpm dlx @wui-design/cli@latest add @wui/toolbar
components/ui/toolbar.tsx
"use client"

import * as React from "react"
import { Toolbar as ToolbarPrimitive } from "radix-ui"
import type { VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
import {
  ToggleGroupIndicator,
  toggleGroupItemClassName,
  useToggleGroupSelection,
} from "@/components/ui/toggle-group"

type ToolbarToggleStyle = VariantProps<typeof toggleVariants>

interface ToolbarGroupContextValue extends ToolbarToggleStyle {
  type: "single" | "multiple"
  selected: string[]
  layoutId: string
}

const ToolbarGroupContext = React.createContext<ToolbarGroupContextValue>({
  type: "single",
  selected: [],
  layoutId: "wui-toolbar-group",
})

export interface ToolbarProps extends React.ComponentProps<
  typeof ToolbarPrimitive.Root
> {
  /** 是否将 Toolbar 的行为合并到唯一的子元素上。 @default false */
  asChild?: boolean
  /** 工具项的排列方向。 @default "horizontal" */
  orientation?: "horizontal" | "vertical"
}

/** 协调一组紧凑操作的方向键导航与排列,不重复实现按钮外观。 */
function Toolbar({
  className,
  orientation = "horizontal",
  ...props
}: ToolbarProps) {
  return (
    <ToolbarPrimitive.Root
      data-slot="toolbar"
      orientation={orientation}
      className={cn(
        "border-border bg-background shadow-xs inline-flex w-fit items-center gap-1 rounded-lg border p-1",
        orientation === "vertical" && "flex-col items-stretch",
        className
      )}
      {...props}
    />
  )
}

export type ToolbarGroupProps = React.ComponentProps<
  typeof ToolbarPrimitive.ToggleGroup
> & {
  /** 组内 ToolbarToggleItem 的统一外观。 @default "default" */
  variant?: ToolbarToggleStyle["variant"]
  /** 组内 ToolbarToggleItem 的统一尺寸。 @default "sm" */
  size?: ToolbarToggleStyle["size"]
}

/**
 * 工具栏内的单选或多选切换组。单选时选中背景在选项之间滑动,
 * 多选时各选项背景独立缩放淡入。
 */
function ToolbarGroup({
  className,
  variant = "default",
  size = "sm",
  children,
  ...props
}: ToolbarGroupProps) {
  const { layoutId, selected, handleValueChange } = useToggleGroupSelection(
    props as Parameters<typeof useToggleGroupSelection>[0]
  )

  const rootProps = {
    ...props,
    onValueChange: handleValueChange,
  } as React.ComponentProps<typeof ToolbarPrimitive.ToggleGroup>

  return (
    <ToolbarPrimitive.ToggleGroup
      data-slot="toolbar-group"
      className={cn("flex items-center gap-0.5", className)}
      {...rootProps}
    >
      <ToolbarGroupContext.Provider
        value={{ variant, size, type: props.type, selected, layoutId }}
      >
        {children}
      </ToolbarGroupContext.Provider>
    </ToolbarPrimitive.ToggleGroup>
  )
}

export interface ToolbarToggleItemProps
  extends React.ComponentProps<typeof ToolbarPrimitive.ToggleItem>,
    ToolbarToggleStyle {}

/** ToolbarGroup 中的切换项,复用 Toggle 的外观与选中指示动效。 */
function ToolbarToggleItem({
  className,
  variant,
  size,
  value,
  children,
  ...props
}: ToolbarToggleItemProps) {
  const context = React.useContext(ToolbarGroupContext)

  return (
    <ToolbarPrimitive.ToggleItem
      data-slot="toolbar-toggle-item"
      value={value}
      className={cn(
        toggleVariants({
          variant: variant ?? context.variant,
          size: size ?? context.size,
        }),
        toggleGroupItemClassName,
        className
      )}
      {...props}
    >
      <ToggleGroupIndicator
        active={context.selected.includes(value)}
        layoutId={
          context.type === "single" ? `${context.layoutId}-indicator` : undefined
        }
      />
      {children}
    </ToolbarPrimitive.ToggleItem>
  )
}

export interface ToolbarButtonProps extends Omit<
  React.ComponentProps<typeof ToolbarPrimitive.Button>,
  "asChild"
> {
  /** 已有的 Button、链接按钮或其他单一交互元素。 */
  children: React.ReactElement
}

/** 将已有按钮接入 Toolbar 的 roving focus,不接管按钮视觉。 */
function ToolbarButton({ children, ...props }: ToolbarButtonProps) {
  return (
    <ToolbarPrimitive.Button asChild data-slot="toolbar-button" {...props}>
      {children}
    </ToolbarPrimitive.Button>
  )
}

/**
 * 分隔相邻的工具项。Radix 会为分隔线设置与工具栏垂直的 orientation:
 * 横向工具栏中为 `vertical`(竖线),纵向工具栏中为 `horizontal`(横线)。
 */
function ToolbarSeparator({
  className,
  ...props
}: React.ComponentProps<typeof ToolbarPrimitive.Separator>) {
  return (
    <ToolbarPrimitive.Separator
      data-slot="toolbar-separator"
      className={cn(
        "bg-border mx-1 h-5 w-px shrink-0 data-[orientation=horizontal]:mx-0 data-[orientation=horizontal]:my-1 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full",
        className
      )}
      {...props}
    />
  )
}

export {
  Toolbar,
  ToolbarButton,
  ToolbarGroup,
  ToolbarSeparator,
  ToolbarToggleItem,
}

使用场景

Toolbar 用于编辑器、数据表和图表标题旁的一组相关操作。ToolbarButton 通过 asChild 把已有 Button 接入 Radix Toolbar 的方向键导航, 不会再造一套按钮尺寸、颜色或焦点样式。下拉选择器等复杂控件可以直接与 ToolbarButton 并列组合。

需要保持选中状态的格式按钮(加粗、对齐方式等)使用 ToolbarGroup + ToolbarToggleItem:

  • ToolbarGroup 需要声明 type="single" | "multiple",支持 value / defaultValue / onValueChange,并通过 variant、size(默认 sm)统一组内外观。
  • ToolbarToggleItem 复用 Toggle 的样式:单选组的选中背景会在选项之间平滑滑动,多选组的每个选项背景独立缩放淡入;系统开启“减少动态效果”时直接切换。
  • 组内选项与 ToolbarButton 共享同一套方向键漫游焦点。
  • ToolbarSeparator 自动与工具栏方向垂直:横向工具栏显示为竖线,纵向工具栏显示为横线。

属性

属性类型默认值说明
asChildbooleanfalse是否将 Toolbar 的行为合并到唯一的子元素上。
orientation"horizontal" | "vertical"horizontal工具项的排列方向。

事件

工具栏只协调布局和键盘导航,各个按钮、链接和选择器继续处理自己的业务事件。