Skip to content

feat(checkbox): support button-style variant for checkbox group - #2600

Closed
ceilf6 wants to merge 2 commits into
Tencent:developfrom
ceilf6:feature/issue-2562
Closed

feat(checkbox): support button-style variant for checkbox group#2600
ceilf6 wants to merge 2 commits into
Tencent:developfrom
ceilf6:feature/issue-2562

Conversation

@ceilf6

@ceilf6 ceilf6 commented Jul 2, 2026

Copy link
Copy Markdown

背景

Closes #2562

多选框组支持按钮风格——选项以标签式按钮横向/纵向排列,选中项以深色填充突出,未选中项为浅色描边。适用于 CRM/OA 系统的权限分配、功能开关等选项较多、需要强反馈的场景(见 issue 描述)。

改动内容

tdesign-common 中补齐按钮风格的 样式与设计文档(UI 设计部分),参考已有的 radio-button 实现以保持两个组件一致。

样式 style/web/components/checkbox/

  • 新增 t-checkbox-button 类,以及在 t-checkbox-group 上的修饰符:__outline(描边)、--filled(填充-默认)、--primary-filled(主色填充)、--vertical(纵向)
  • 复用全局 token,新增 @checkbox-button-* 语义变量(命名与 @radio-button-* 对齐)
  • 支持 s / m / l 三种尺寸
  • 完整状态:checked / indeterminate / disabled / disabled-checked / disabled-indeterminate / hover
  • 多选适配:填充态选中背景直接落在每个按钮上(radio 的滑动色块色块依赖单选语义,不适用多选);描边态沿用 radio 的 border-collapse 机制(选中项补回右侧描边 + 隐藏紧邻后续项左侧描边),相邻多选选中项不会出现双线
  • 半选(indeterminate)态:在主色填充下用 brand-hover 区分,反色字仍可读

文档

  • docs/web/api/checkbox.md + checkbox.en-US.md:新增「按钮风格的多选框组」「不同尺寸」「不同形态」示例占位({{ button }} / {{ button-size }} / {{ button-variant }}),具体 _example/ 文件由各框架仓库填充
  • docs/web/design/checkbox.md:新增按钮风格的使用场景与三种形态说明

范围说明

按 tdesign-common 的职责划分,本仓库只承载样式与设计文档;组件的 props / template / example 代码将随后在 tdesign-vue-next / tdesign-react 仓库消费这些样式(后续 PR)。

校验

  • npx stylelint style/web/components/checkbox/*.less 通过
  • npx prettier --check 通过(含 markdown)
  • lessc 编译通过,生成的 CSS 选择器符合预期
  • 本地用主题 token 渲染了 outline / filled / primary-filled / vertical / 三种尺寸 / 全状态预览,视觉正确

🤖 Generated with Claude Code

Add a button variant for the checkbox group (issue Tencent#2562), mirroring the
existing radio-button pattern so the two components stay consistent.

- style: add t-checkbox-button + group modifiers (outline / filled /
  primary-filled / vertical), s/m/l sizes, and full states including
  checked / indeterminate / disabled / disabled-checked
- style: reuse global tokens via new @checkbox-button-* semantics in
  _var.less; multi-select filled variant uses per-button background
  instead of radio's sliding bg-block
- docs: add button-style sections to web api (zh/en) and design doc

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 2, 2026 03:51
@ceilf6

ceilf6 commented Jul 2, 2026

Copy link
Copy Markdown
Author

@ceilf6/repo-guard CR

@ceilf6

ceilf6 commented Jul 2, 2026

Copy link
Copy Markdown
Author

🛡️ ceilf6/repo-guard

代码评审报告: feat(checkbox): support button-style variant for checkbox group

风险等级:
处理建议: 请求修改
决策摘要: 样式与设计文档本身可合并,但存在若干类名/选择器实现缺陷(vertical 修饰符类名不匹配、filled 模式下选中态遮罩 z-index/分割线逻辑疑似有 bug、变量命名歧义),建议修正后再合入,避免消费方(vue-next/react)后续实现时踩坑。

级联分析

  • 变更符号: .t-checkbox-group__outline.t-checkbox-group--filled.t-checkbox-group--primary-filled.t-checkbox-group--vertical.t-checkbox-button 及一批 @checkbox-button-* less 变量 (text search)
  • 受影响流程: 无直接调用方,本仓库(tdesign-common)只产出样式与变量,供 tdesign-vue-next / tdesign-react 后续 PR 消费 (inferred)
  • 变更集外调用方: 无,本 PR 明确声明只做样式层,不含组件模板/props (text search)
  • 置信度: medium — 无代码图谱,且下游消费者尚不存在(后续 PR),因此无法验证类名/变量与实际组件实现的对齐程度,只能基于 CSS/Less 静态推理和与 radio-button 的类比进行审查。

问题发现

  1. [高] --vertical 修饰符类名与实际 DOM class 命名规则不一致,可能导致纵向样式永远不生效

    • 证据: _index.less 中新增选择器为 &.@{checkbox-cls}-group--vertical(第 89 行附近),而本文件中所有其他 modifier 均使用 BEM 风格 __outline / --filled / --primary-filled 混用(__outline 用双下划线,--filled/--primary-filled/--vertical 用双连字符),没有统一规则文档说明为什么 outline 是 __ 而其余是 --。radio-button 中对应实现如果统一用某一种前缀,这里的不一致会让消费方在写模板时难以确定该拼哪个 class。
    • 受影响调用方/流程: 后续 tdesign-vue-next / tdesign-react PR 在拼接 className 时,如果对照 radio-button 抄错前缀(__ vs --),会导致样式完全不生效且难以排查。
    • 最小可行修复: 在 PR 描述或代码注释中明确列出全部 modifier class 名及其命名规则来源(是否与 radio-button 完全一致),并在 _var.less_index.less 顶部加注释固化契约,避免消费方猜测。
  2. [中] filled 模式下选中态分割线隐藏逻辑存在遗漏分支

    • 证据: _index.less 246-260 行左右,&.@{prefix}-is-checked&.@{prefix}-is-indeterminate 都各自处理了 & + .@{checkbox-button--cls}::before { opacity: 0 }(隐藏下一个按钮的左分割线),但没有处理"当前按钮自身左侧分割线"在其前一个兄弟为 checked/indeterminate 时的隐藏 — 这个逻辑依赖前一个元素的选择器写在后一个元素身上,是对的;但如果连续三个按钮全部选中(常见多选场景),第二个按钮的 ::before 被第一个按钮的 + 选择器隐藏,同时第二个按钮自身也因为 is-checked 设置了 ::before { opacity: 0 }(因为它自己也是 checked,第 234-236 行的 &.is-checked::before { opacity: 0 } 对自身生效),逻辑重叠但无害;然而未选中按钮夹在两个选中按钮之间的场景(例如 [checked, unchecked, checked])未被验证:中间 unchecked 按钮的 ::before 会被第一个 checked 按钮的 + 选择器隐藏,但它自身右侧与第三个 checked 按钮之间的分割线(即第三个按钮的 ::before)不会被隐藏,视觉上应该正常显示分割线,这点是对的,无需修复。该 finding 降级为观察项:请在合入前用 [checked, unchecked, checked] 交替选中态跑一遍视觉验证,PR 描述中的“全状态预览”未明确提及是否覆盖了交替选中场景。
    • 受影响调用方/流程: filled 模式的视觉正确性,尤其是交替选中场景。
    • 最小可行修复: 在 PR 校验清单中补充“交替选中/未选中”的视觉回归截图,或在 CI 视觉测试中补充该用例。
  3. [中] @checkbox-button-color-primary-filled-indeterminate 复用 @brand-color-hover 语义不清晰

    • 证据: _var.less 第 90-91 行注释写道“半选态用 brand-hover,比选中态略浅以作区分”,但 @brand-color-hover 语义上是“hover 交互态颜色”,用作“半选(indeterminate)静态展示色”属于语义借用,如果未来主题定制只想调整 hover 反馈强度而不想影响 indeterminate 展示,会产生耦合修改。
    • 受影响调用方/流程: 主题定制、暗色模式适配时容易因语义混用产生非预期视觉变化。
    • 最小可行修复: 新增独立语义变量(如 @checkbox-button-bg-color-primary-filled-indeterminate-base: @brand-color-8 或类似 token),而不是直接复用 @brand-color-hover;如果这是与 radio-button 保持一致的既有做法,请在注释中说明是复用现有惯例而非本 PR 新引入的耦合。
  4. [低] vertical + filled 组合下 &:last-child { border: 0 } 与 outline 模式的 border-right 处理不对称

    • 证据: _index.less 第 116-120 行(vertical + filled 组合),对 :last-child 直接置 border: 0,而同一 vertical 块中 outline 分支(第 74-99 行)对 :last-child 走的是 border-radius 圆角处理、未清零 border。两种子模式下 :last-child 处理方式不同,虽然结果视觉上可能都正确(filled 本来就无描边),但读者需要分别理解两套逻辑,增加认知成本。
    • 受影响调用方/流程: 无功能性影响,仅可读性/一致性。
    • 最小可行修复: 可选,非阻塞;如时间允许可在两个分支加简短注释说明为何处理方式不同(一个是描边框需要计算圆角与合并线,一个是无边框可直接清零)。

行级发现

  • [style/web/components/checkbox/_index.less:89] &.@{checkbox-cls}-group--vertical 使用双连字符命名,而同文件 outline 变体用 __outline(双下划线),命名规则不统一,需要在注释或文档中明确契约来源,避免消费方拼错 class。
  • [style/web/components/checkbox/_index.less:120] vertical + filled&:last-child { border: 0 } 与 outline 分支对 :last-child 的处理方式不对称,建议补充注释说明设计意图。
  • [style/web/components/checkbox/_index.less:234] &.@{prefix}-is-checked 在 filled 模式下同时设置自身 ::before opacity: 0 和影响下一个兄弟的 ::before,建议补充交替选中态([checked, unchecked, checked])的视觉验证用例。
  • [style/web/components/checkbox/_var.less:91] @checkbox-button-color-primary-filled-indeterminate 复用 @brand-color-hover 语义,建议改用独立语义变量或在注释中明确这是沿用 radio-button 既有惯例。

Karpathy 评审

  • 假设: 本 PR 假设消费方(vue-next/react)会严格按照当前定义的 class 名称拼接组件,但由于 __outline--filled/--vertical 命名不统一,这是一个隐藏假设风险,容易在跨仓库协作时产生实现偏差。issue [Checkbox] 多选框组支持按钮风格 #2562 的验收标准("选中后背景颜色改变即选中的 item")已被样式覆盖,但 issue 未明确要求 outline/filled/primary-filled 三种形态,PR 在 issue 基础上做了合理扩展(参考 radio-button 保持一致性),扩展范围可接受。
  • 简洁性: 三种形态(outline/filled/primary-filled)+ 纵向排列的组合复用了 radio-button 的既有变量命名模式,整体复杂度可控;未见明显猜测性抽象或过度配置。
  • 结构质量: 样式文件从 19 行增长到约 341 行(+320),文件本身规模仍在可维护范围内(未接近 1000 行阈值),但modifier 命名不统一是可维护性隐患,长期看会增加后续贡献者的心智负担。
  • 变更范围: 变更集中在 tdesign-common 的样式与文档层,未夹带无关重构,范围符合 PR 描述的"仅承载样式与设计文档"承诺,是外科手术式变更。
  • 验证: PR 描述提及本地视觉预览覆盖了 outline/filled/primary-filled/vertical/三种尺寸/全状态,但未提及交替选中态([checked, unchecked, checked])等边界组合是否验证过,也没有自动化视觉回归测试(该仓库可能确实没有此类基础设施,视为可接受的现状)。

缺失覆盖

  • 交替选中/未选中状态下(尤其 outline 与 filled 模式的分割线/背景块拼接逻辑)的视觉验证截图或用例。
  • 消费方(vue-next/react)后续 PR 依赖的 class 命名契约缺少显式文档锚点,建议在本 PR 或后续 PR 中补充一份"按钮风格 class 命名规范"说明,降低跨仓库实现风险。

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a button-style visual variant for checkbox groups at the tdesign-common layer by introducing the required Less tokens/styles and updating the design/API docs to include the new variant’s documentation placeholders, aligning behavior and naming with the existing radio-button patterns.

Changes:

  • Added button-style checkbox group semantic variables (@checkbox-button-*) to support outline/filled/primary-filled, sizes, and full state coverage.
  • Implemented button-style checkbox group styles in checkbox/_index.less, including outline border-collapsing behavior, filled variants, and a vertical layout mode.
  • Updated checkbox design docs and API docs (CN/EN) with new sections and example placeholders for button-style checkbox groups.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
style/web/components/checkbox/_var.less Introduces semantic tokens for button-style checkbox groups (colors/sizes/padding/transition).
style/web/components/checkbox/_index.less Adds the core button-style checkbox group styling (outline/filled/primary-filled, vertical, sizes).
docs/web/design/checkbox.md Documents button-style checkbox group usage scenarios and variant descriptions.
docs/web/api/checkbox.md Adds CN API doc sections and placeholders for button-style group examples.
docs/web/api/checkbox.en-US.md Adds EN API doc sections and placeholders for button-style group examples.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +110 to +134
width: 100%;
border-right: @checkbox-button-border;
border-right-color: @checkbox-button-border-color-default;
margin-top: -1px;

&:first-child {
margin-top: 0;
border-radius: @checkbox-button-border-radius @checkbox-button-border-radius 0 0;
}

&:last-child {
border-radius: 0 0 @checkbox-button-border-radius @checkbox-button-border-radius;
}

&.@{prefix}-is-checked,
&.@{prefix}-is-indeterminate {
border-color: @checkbox-button-border-color-checked;
border-right-color: @checkbox-button-border-color-checked;
z-index: 1;

+.@{checkbox-button--cls} {
border-left: @checkbox-button-border;
border-left-color: @checkbox-button-border-color-default;
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in f704c64. The vertical+outline block no longer groups is-checked and is-indeterminate under the checked token. Instead:

  • A shared rule handles only stacking + sibling border-left (z-index: 1 + re-add next sibling's left border), applied to checked / indeterminate / disabled-checked / disabled-indeterminate.
  • Separate &.is-checked:not(.is-disabled) and &.is-indeterminate:not(.is-disabled) rules set each state's own border-color token, so indeterminate uses @checkbox-button-border-color-indeterminate and disabled-checked is no longer overridden (it keeps the disabled border from the horizontal outline rule).

The :not(.is-disabled) raises specificity just enough to win over the vertical base's border-right-color: default (same 4-class specificity, source order) without touching the disabled branch.

- vertical outline: split is-checked / is-indeterminate so each uses its
  own border-color token; scope color override with :not(.is-disabled) so
  disabled-checked keeps its disabled border (Copilot review)
- primary-filled indeterminate bg: use @brand-color-6 directly instead of
  reusing @brand-color-hover, decoupling indeterminate from hover semantics
- document the button-variant class-naming contract (mirrors radio-button)
  to prevent consumers from mis-stitching __outline vs --filled prefixes
- comment why vertical+filled :last-child uses border:0 vs outline's radius
- verified alternating [checked, unchecked, checked] in filled/primary-filled

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ceilf6

ceilf6 commented Jul 2, 2026

Copy link
Copy Markdown
Author

Thanks for the detailed review — all findings addressed in `f704c640`.

1. [高] modifier class 命名契约不统一
Added a contract comment at the top of the button-variant block in `_index.less` listing every modifier class (`__outline` / `--filled` / `--primary-filled` / `--vertical`) and the button element class (`.t-checkbox-button`), explicitly noting the prefix split is inherited from `radio-button` (not newly introduced) so consumers can stitch classes confidently. If the prefixes should be unified, that's a cross-component change to make together with radio-button.

2. [中] 交替选中态视觉验证
Added `[checked, unchecked, checked, unchecked, checked]` cases to the local preview for both filled and primary-filled. Verified: separators (`::before`) only render between two adjacent unchecked buttons; any separator touching a checked/indeterminate pill is hidden, so alternating selection produces clean pills with no stray lines and no double borders. Outline alternating was already covered by the border-collapse logic (separator takes the adjacent checked button's brand color).

3. [中] indeterminate 复用 `@brand-color-hover` 语义
`@checkbox-button-bg-color-primary-filled-indeterminate` now references `@brand-color-6` directly instead of `@brand-color-hover`, so a theme tweaking only hover feedback no longer affects the indeterminate display. Raw numbered tokens are already used elsewhere (`chat`, `layout`), so this is consistent with repo conventions.

4. [低] vertical+filled `:last-child` 与 outline 不对称
Added a comment explaining the asymmetry: filled buttons are borderless (`border: 0`), so the last item just clears its border; outline keeps its border and must compute the bottom radius + merged line.

Copilot inline (line 134)
Replied in thread — split the vertical+outline active-state rule so indeterminate uses its own border-color token and disabled-checked is no longer overridden (via `:not(.is-disabled)`).

`stylelint` / `prettier` / `lessc` still pass.

@uyarn

uyarn commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

谢谢参与犀牛鸟活动,讨论后这个issue 决定采纳 #2618 继续推进

@uyarn uyarn closed this Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Checkbox] 多选框组支持按钮风格

4 participants