第十四章 UI 依赖构建:Collapsible 与 Tooltip
在第十三章,我们成功打通了从 API 到 useNavData Hook 的完整数据流。现在,我们的数据已经万事俱备,只欠渲染它的 UI 组件。
NavVertical (我们的最终目标) 依赖两个核心的 src/ui 原子组件来实现其交互:
Collapsible:用于 NavGroup 和 NavList 的展开与折叠。Tooltip:用于 NavItem 中 caption(小字提示)的悬浮显示。
本章的任务,就是遵循我们在第八章(Button)和第九章(Input/Label)建立的 CDD(组件驱动开发) 流程,将这两个“积木”添加到我们的 UI 库中。
14.1 任务:CDD 构建 (一) - Collapsible
Collapsible(可折叠面板)是 NavList 和 NavGroup 能够“展开”和“收起”的 功能基石。我们将严格按照 CDD 范式 (add -> Story -> Refactor -> Docs) 来构建它。
14.1.1. (Add) 添加组件基础
我们首先使用 shadcn/ui 的 CLI 命令来添加组件的“骨架”。
1
| pnpm dlx shadcn@latest add collapsible
|
执行此命令后,shadcn 会为我们创建 src/components/ui/collapsible.tsx 文件。正如我们在第九章所配置的,它默认只会为我们生成一个 纯粹的、无样式的 Radix UI 重导出:
文件路径: src/components/ui/collapsible.tsx (初始)
1 2 3 4 5 6 7
| import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
架构思考:这种“最小化”的初始文件是 shadcn/ui 理念的完美体现。它 只提供功能(来自 @radix-ui/react-collapsible),而将 所有样式(className)的决定权 交还给我们。我们接下来的任务,就是为这个“骨架”穿上“皮肤”。
14.1.2. (Story) Storybook 先行
在修改组件源码之前,我们必须先在 Storybook 中“看见”它。我们需要一个可视化的“画布”来验证我们后续的样式修改。
我们将创建 collapsible.stories.tsx 文件,并编写一个基础的 Default 故事。
创建文件:
1
| touch src/components/ui/collapsible/collapsible.stories.tsx
|
编写故事:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
|
import type { Meta, StoryObj } from '@storybook/react-vite'; import { Button } from '../button/button'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from './collapsible';
const meta: Meta<typeof Collapsible> = { title: 'UI/Collapsible', component: Collapsible, parameters: { layout: 'centered', }, tags: ['autodocs'], argTypes: { open: { control: 'boolean', description: '受控:是否展开', }, disabled: { control: 'boolean', description: '是否禁用', }, }, };
export default meta; type Story = StoryObj<typeof meta>;
export const Default: Story = { args: { }, render: (args) => ( <Collapsible {...args} className="w-[350px] space-y-2"> <CollapsibleTrigger asChild> {/* 我们使用 Button 作为触发器 */} <Button variant="outline">Toggle Content</Button> </CollapsibleTrigger>
{/* [关键] 此时,我们的 CollapsibleContent 还没有样式。 我们手动添加一些 Tailwind 类,以便能在 Storybook 中“看见”它。 */} <CollapsibleContent> <div className="rounded-md border px-4 py-3 font-mono text-sm shadow-sm"> @radix-ui/colors </div> <div className="rounded-md border px-4 py-3 font-mono text-sm shadow-sm mt-2"> @radix-ui/icons </div> </CollapsibleContent> </Collapsible> ), };
|
验证(“RED”):现在运行 pnpm storybook。你会看到一个 Collapsible 组件,但当你点击按钮时,内容的出现和消失是 瞬时 的、没有动画 的。
这就是我们的“RED”状态。我们的目标(“GREEN”)是让它拥有平滑展开/折叠动画。
14.1.3. 编码实现:样式抽离与动画注入
遵循 Button 组件的架构模式,我们需要将样式抽离到独立的 variants 文件中。我们的 index.css 已经通过 @plugin "tailwindcss-animate" 引入了 collapsible-down 和 collapsible-up 动画(见 9.2.3 节)。
步骤 1:创建样式抽离文件
首先创建 collapsible.variants.tsx,使用 cva 来定义 CollapsibleContent 的样式:
文件路径: src/components/ui/collapsible/collapsible.variants.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { cva } from 'class-variance-authority';
export const collapsibleContentVariants = cva( 'overflow-hidden text-sm transition-all', { variants: { }, defaultVariants: {}, }, );
export const collapsibleAnimationClasses = { open: 'data-[state=open]:animate-collapsible-down', closed: 'data-[state=closed]:animate-collapsible-up', } as const;
|
架构思考:虽然 Collapsible 目前不需要像 Button 那样复杂的变体系统,但我们依然遵循相同的架构模式:
- 使用
cva 定义基础样式。 - 将动画类抽离为常量,便于维护和理解。
- 为未来的扩展(如不同的动画效果)预留空间。
🤔 技术深挖:动画是如何工作的?
你可能会好奇:为什么只需要添加 data-[state=open]:animate-collapsible-down 这样的类名,就能实现平滑的折叠动画?这背后涉及三个关键技术的巧妙配合:
1. tailwindcss-animate 的预设动画
当你在 index.css 中添加 @plugin "tailwindcss-animate" 后,这个插件会自动注册 CSS 关键帧动画和对应的 Tailwind 工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @keyframes collapsible-down { from { height: 0; } to { height: var(--radix-collapsible-content-height); } }
@keyframes collapsible-up { from { height: var(--radix-collapsible-content-height); } to { height: 0; } }
.animate-collapsible-down { animation: collapsible-down 0.2s ease-out; }
.animate-collapsible-up { animation: collapsible-up 0.2s ease-out; }
|
2. Radix UI 的 data-[state] 属性
@radix-ui/react-collapsible 会根据组件的打开/关闭状态,自动在 DOM 上添加不同的 data-state 属性:
1 2 3 4 5
| <div data-state="open" class="...">内容</div>
<div data-state="closed" class="...">内容</div>
|
这是 Radix UI 的设计哲学:通过数据属性而非类名来表示状态。
3. Tailwind 的 data-[] 属性选择器
Tailwind v3.4+ 支持 data-[attribute=value]: 前缀语法,它会被编译成标准的 CSS 属性选择器:
1 2 3 4
| [data-state="open"] { animation: collapsible-down 0.2s ease-out; }
|
完整的工作流程:
1 2 3 4 5 6 7 8 9 10 11
| 用户点击 CollapsibleTrigger ↓ Radix UI 改变内部状态 (open ↔ closed) ↓ Radix UI 更新 DOM 的 data-state 属性 ↓ 浏览器检测到属性变化,触发对应的 CSS 选择器 ↓ 执行 collapsible-down/up 动画 ↓ 用户看到平滑的折叠效果
|
为什么使用 --radix-collapsible-content-height?
因为内容的高度是 动态的(取决于内容多少),Radix UI 会在运行时测量实际高度,并将结果存储为 CSS 变量。这样,动画就能从 0 过渡到 实际高度,而无需硬编码固定值。
这就是现代 UI 库的 “约定大于配置” ——三方都遵循同一套约定,你只需要 “组装” 它们,就能获得开箱即用的动画效果!
步骤 2:创建主组件文件
现在修改 collapsible.tsx,导入并使用抽离的样式:
文件路径: src/components/ui/collapsible/collapsible.tsx (重构)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; import * as React from 'react';
import { cn } from '@/utils/cn'; import { collapsibleAnimationClasses, collapsibleContentVariants, } from './collapsible.variants';
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
type CollapsibleContentElement = React.ComponentRef<typeof CollapsiblePrimitive.CollapsibleContent>; type CollapsibleContentProps = React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.CollapsibleContent>;
const CollapsibleContent = React.forwardRef< CollapsibleContentElement, CollapsibleContentProps >(({ className, children, ...props }, ref) => ( <CollapsiblePrimitive.CollapsibleContent ref={ref} className={cn( collapsibleContentVariants(), // 基础样式来自 variants collapsibleAnimationClasses.open, // 展开动画 collapsibleAnimationClasses.closed, // 折叠动画 className, // 允许外部覆盖 )} {...props} > {children} </CollapsiblePrimitive.CollapsibleContent> ));
CollapsibleContent.displayName = CollapsiblePrimitive.CollapsibleContent.displayName;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
验证(“GREEN”):保存文件。Storybook 会自动热更新。现在再次访问 UI/Collapsible 故事,点击 “Toggle Content” 按钮——你将看到内容区 平滑地展开和折叠 了。
14.1.4. (Docs) 文档深化:collapsible.mdx
最后,我们为它创建 MDX 文档,使其符合“Button 范式”的交付标准。
创建文件:
1
| touch src/components/ui/collapsible/collapsible.mdx
|
编写文档:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { Meta, Story, Controls } from '@storybook/blocks'; import * as CollapsibleStories from './collapsible.stories';
<Meta of={CollapsibleStories} />
# Collapsible 可折叠面板
一个可以展开或折叠内容区域的组件。
## 基础用法
`Collapsible` 组件基于 `@radix-ui/react-collapsible` 封装,提供了无障碍访问和灵活的 API。它包含三个核心部分:
- `Collapsible`:根组件,包裹所有内容。 - `CollapsibleTrigger`:触发器,点击它会改变展开/折叠状态。 - `CollapsibleContent`:内容区,它会自动应用 `collapsible-down` 和 `collapsible-up` 动画。
<Story of={CollapsibleStories.Default} />
## API 参考 (Props)
`Collapsible` 根组件的 `props`:
<Controls />
|
清理 autodocs:最后,回到 collapsible.stories.tsx,移除 tags: ['autodocs'],让 MDX 文件接管文档页。
1 2 3 4 5 6
| const meta: Meta<typeof Collapsible> = { };
|
任务 14.1 完成! 我们已经将 Collapsible 完美集成到了我们的 src/ui 库中,并拥有了动画、Storybook 预览和 MDX 文档。
Tooltip(工具提示)是 NavItem 在侧边栏折叠状态下展示 caption(说明文字)的关键组件。当用户悬停在导航项上时,Tooltip 会优雅地浮现出来,提供额外的上下文信息。
14.2.1. (Add) 添加组件基础
同样使用 shadcn/ui CLI 添加 Tooltip 组件骨架:
1
| pnpm dlx shadcn@latest add tooltip
|
执行后,shadcn 会创建 src/components/ui/tooltip/tooltip.tsx 文件,提供基础的 Radix UI 重导出:
文件路径: src/components/ui/tooltip/tooltip.tsx (初始)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import * as React from "react" import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef< React.ElementRef<typeof TooltipPrimitive.Content>, React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> >(({ className, sideOffset = 4, ...props }, ref) => ( <TooltipPrimitive.Portal> <TooltipPrimitive.Content ref={ref} sideOffset={sideOffset} className={cn( "z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]", className )} {...props} /> </TooltipPrimitive.Portal> )) TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
架构观察:与 Collapsible 不同,shadcn 为 Tooltip 提供了一些默认样式。但这些样式都 硬编码在组件内部,违背了我们 “样式与逻辑分离” 的架构原则。我们需要将它们重构到 variants 文件中。
14.2.2. (Story) Storybook 先行
创建 Storybook 故事,先 “看见” 当前的 Tooltip:
创建文件:
1
| touch src/components/ui/tooltip/tooltip.stories.tsx
|
编写故事:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
|
import type { Meta, StoryObj } from '@storybook/react-vite'; import { Button } from '../button/button'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from './tooltip';
const meta: Meta<typeof Tooltip> = { title: 'UI/Tooltip', component: Tooltip, parameters: { layout: 'centered', }, tags: ['autodocs'], argTypes: { open: { control: 'boolean', description: '受控:是否显示 Tooltip', }, defaultOpen: { control: 'boolean', description: '非受控:默认是否显示', }, delayDuration: { control: 'number', description: '悬停多久后显示(毫秒)', }, }, };
export default meta; type Story = StoryObj<typeof meta>;
export const Default: Story = { args: {}, render: (args) => ( <TooltipProvider> <Tooltip {...args}> <TooltipTrigger asChild> <Button variant="outline">Hover me</Button> </TooltipTrigger> <TooltipContent> <p>这是一个工具提示</p> </TooltipContent> </Tooltip> </TooltipProvider> ), };
export const Sides: Story = { render: () => ( <TooltipProvider> <div className="flex gap-4"> <Tooltip> <TooltipTrigger asChild> <Button variant="outline">Top</Button> </TooltipTrigger> <TooltipContent side="top"> <p>顶部提示</p> </TooltipContent> </Tooltip>
<Tooltip> <TooltipTrigger asChild> <Button variant="outline">Right</Button> </TooltipTrigger> <TooltipContent side="right"> <p>右侧提示</p> </TooltipContent> </Tooltip>
<Tooltip> <TooltipTrigger asChild> <Button variant="outline">Bottom</Button> </TooltipTrigger> <TooltipContent side="bottom"> <p>底部提示</p> </TooltipContent> </Tooltip>
<Tooltip> <TooltipTrigger asChild> <Button variant="outline">Left</Button> </TooltipTrigger> <TooltipContent side="left"> <p>左侧提示</p> </TooltipContent> </Tooltip> </div> </TooltipProvider> ), };
|
验证(“RED”):运行 pnpm storybook。你会看到 Tooltip 可以正常工作,但所有样式都写在组件内部,不符合我们的架构标准。
14.2.3. 编码实现:样式抽离与动画优化
步骤 1:创建样式抽离文件
创建 tooltip.variants.tsx,使用 cva 管理样式:
文件路径: src/components/ui/tooltip/tooltip.variants.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| import { cva } from 'class-variance-authority';
export const tooltipContentVariants = cva( [ 'z-50', 'overflow-hidden', 'rounded-md', 'px-3 py-1.5', 'text-xs', 'bg-primary', 'text-primary-foreground', 'origin-[--radix-tooltip-content-transform-origin]', ], { variants: { }, defaultVariants: {}, }, );
export const tooltipAnimationClasses = { in: [ 'animate-in', 'fade-in-0', 'zoom-in-95', ].join(' '), out: [ 'data-[state=closed]:animate-out', 'data-[state=closed]:fade-out-0', 'data-[state=closed]:zoom-out-95', ].join(' '), side: [ 'data-[side=bottom]:slide-in-from-top-2', 'data-[side=left]:slide-in-from-right-2', 'data-[side=right]:slide-in-from-left-2', 'data-[side=top]:slide-in-from-bottom-2', ].join(' '), } as const;
|
架构思考:
Tooltip 的动画比 Collapsible 更复杂,它包含三个层次:
- 入场动画:淡入 + 缩放 (
fade-in + zoom-in) - 出场动画:淡出 + 缩放 (
fade-out + zoom-out) - 方向动画:根据 Tooltip 的显示位置(上/下/左/右),从对应方向滑入
我们将这三类动画分别抽离为常量,保持代码的可读性和可维护性。
步骤 2:重构主组件文件
修改 tooltip.tsx,导入并使用抽离的样式:
文件路径: src/components/ui/tooltip/tooltip.tsx (重构)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import * as React from 'react'; import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cn } from '@/utils/cn'; import { tooltipAnimationClasses, tooltipContentVariants, } from './tooltip.variants';
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
type TooltipContentElement = React.ComponentRef<typeof TooltipPrimitive.Content>; type TooltipContentProps = React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>;
const TooltipContent = React.forwardRef< TooltipContentElement, TooltipContentProps >(({ className, sideOffset = 4, ...props }, ref) => ( <TooltipPrimitive.Portal> <TooltipPrimitive.Content ref={ref} sideOffset={sideOffset} className={cn( tooltipContentVariants(), // 基础样式来自 variants tooltipAnimationClasses.in, // 入场动画 tooltipAnimationClasses.out, // 出场动画 tooltipAnimationClasses.side, // 方向滑入动画 className, // 允许外部覆盖 )} {...props} /> </TooltipPrimitive.Portal> ));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
验证(“GREEN”):保存文件,Storybook 自动热更新。现在访问 UI/Tooltip 故事,悬停在按钮上——你将看到 Tooltip 以优雅的动画弹出,样式已经完全从组件中抽离出来了。
创建 MDX 文档:
创建文件:
1
| touch src/components/ui/tooltip/tooltip.mdx
|
编写文档:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| import { Meta, Story, Controls } from '@storybook/blocks'; import * as TooltipStories from './tooltip.stories';
<Meta of={TooltipStories} />
# Tooltip 工具提示
一个悬浮提示组件,用于展示额外的说明信息。
## 基础用法
`Tooltip` 组件基于 `@radix-ui/react-tooltip` 封装,提供了完整的无障碍支持。它包含四个核心部分:
- `TooltipProvider`:提供全局配置(如延迟时长)的上下文,通常包裹在应用的顶层。 - `Tooltip`:根组件,管理单个 Tooltip 的状态。 - `TooltipTrigger`:触发器,用户悬停在它上面时显示 Tooltip。 - `TooltipContent`:内容区,显示提示信息,支持四个方向 (`top` / `right` / `bottom` / `left`)。
<Story of={TooltipStories.Default} />
## 显示方向
Tooltip 支持四个方向显示,通过 `side` prop 控制:
<Story of={TooltipStories.Sides} />
## API 参考 (Props)
`Tooltip` 根组件的 `props`:
<Controls />
## 使用建议
- **简洁性**:Tooltip 内容应简短明了,避免长篇大论。 - **必要性**:只在需要额外说明时使用,不要过度使用导致界面混乱。 - **无障碍性**:Tooltip 内容应该是**补充信息**,而非必须阅读的关键信息,因为它在键盘导航时可能难以触发。
|
清理 autodocs:
1 2 3 4 5 6
| const meta: Meta<typeof Tooltip> = { };
|
任务 14.2 完成! 我们已经将 Tooltip 完美集成到了我们的 UI 库中,并保持了与 Button 和 Collapsible 一致的架构模式。
14.3 本章小结与架构回顾
在本章中,我们成功构建了 NavVertical 组件所需的两个核心 UI 依赖:Collapsible 和 Tooltip。更重要的是,我们通过这个过程 巩固并深化 了我们在第八、九章建立的 CDD(组件驱动开发)架构模式。
14.3.1. 我们做了什么?
📦 交付成果
Collapsible 组件
collapsible.tsx:主组件文件collapsible.variants.tsx:样式抽离文件collapsible.stories.tsx:Storybook 故事collapsible.mdx:组件文档
Tooltip 组件
tooltip.tsx:主组件文件tooltip.variants.tsx:样式抽离文件tooltip.stories.tsx:Storybook 故事tooltip.mdx:组件文档
🏗️ 架构一致性
所有组件都遵循相同的 “三层架构”:
1 2 3 4 5
| 组件目录/ ├── component.tsx # 逻辑层:组件行为、props、ref 转发 ├── component.variants.tsx # 样式层:CVA 变体 + 动画常量 ├── component.stories.tsx # 测试层:交互验证、视觉回归 └── component.mdx # 文档层:使用指南、API 参考
|
这种架构让我们的代码具备了:
- ✅ 可维护性:样式修改不触及组件逻辑
- ✅ 可测试性:Storybook 提供可视化验证
- ✅ 可复用性:通过
cva 轻松扩展变体 - ✅ 可文档化:MDX 让文档与代码同步
14.3.2. 关键技术洞察
动画系统的工作原理
我们在 14.1.3 节深入剖析了 tailwindcss-animate + Radix UI + Tailwind 的 “约定配合” 机制:
tailwindcss-animate 提供预设的 CSS 关键帧动画- Radix UI 通过
data-[state] 和 data-[side] 属性暴露组件状态 - Tailwind 的
data-[] 选择器将状态与动画连接起来 - CSS 变量 (如
--radix-collapsible-content-height) 让动画适配动态内容
这种设计让我们能用 声明式的类名 实现复杂的、自适应的动画效果。
样式抽离的层次
| 组件 | 基础样式 | 状态动画 | 复杂度 |
|---|
Button | 6 种 variant + 4 种 size | ❌ 无 | ⭐⭐ |
Collapsible | 单一基础样式 | 2 种状态(open/closed) | ⭐ |
Tooltip | 单一基础样式 | 3 层动画(in/out/side) | ⭐⭐⭐ |
可以看出,即使组件复杂度不同,我们依然能用统一的 variants 模式来管理样式。
14.3.3. 为什么跳过 TDD?
与第八章的 Button 不同,Collapsible 和 Tooltip 我们 跳过了单元测试(TDD),原因是:
- 它们是 Radix UI 的薄封装:核心功能已经在 Radix UI 中被充分测试
- 我们的价值是样式层:我们的重构主要是样式抽离,而样式的正确性通过 Storybook 的视觉验证 更直观
- 避免测试实现细节:测试 “className 是否正确拼接” 属于实现细节,而非行为契约
架构原则:
- 行为复杂的业务组件(如
Button 的交互逻辑) → TDD - UI 封装组件(如
Collapsible、Tooltip) → CDD (Storybook 验证)
14.3.4. 代码入库
现在,让我们将这些成果提交到 Git 仓库:
步骤 1:检查文件变更
你应该看到以下新增文件:
1 2 3 4 5 6 7 8 9 10 11
| src/components/ui/collapsible/ - collapsible.tsx - collapsible.variants.tsx - collapsible.stories.tsx - collapsible.mdx
src/components/ui/tooltip/ - tooltip.tsx - tooltip.variants.tsx - tooltip.stories.tsx - tooltip.mdx
|
步骤 2:暂存文件
步骤 3:提交更改
1 2 3 4 5 6
| git commit -m "feat: add Collapsible and Tooltip UI components" -m "- Add Collapsible component with collapse/expand animations" -m "- Add Tooltip component with multi-directional display" -m "- Extract styles to variants files using CVA pattern" -m "- Add Storybook stories and MDX documentation" -m "- Follow Button architecture for consistency"
|
提交信息解读:
- 类型:
feat(新功能) - 范围:UI 组件
- 主题:简洁说明添加了什么
- 详情:列出关键变更点,方便后续代码审查和历史回溯
步骤 4:验证提交
确认提交记录已成功创建。
14.3.5. 下一步预告
在第十五章,我们将迎来本项目的 第一个复杂业务组件:NavVertical(垂直导航菜单)。
它将整合我们目前构建的所有 “积木”:
1 2 3 4 5 6 7
| NavVertical (第十五章) ├── 使用 useNavData Hook (第十三章) ├── 渲染 NavGroup 和 NavList │ └── 使用 Collapsible (第十四章) └── 渲染 NavItem ├── 使用 Button 样式 (第八章) └── 使用 Tooltip 展示 caption (第十四章)
|
这将是我们对 组件组合(Composition) 能力的一次集大成检验。准备好了吗?让我们继续前进!