From 43a2194281815411ab9d1c414a3e2a31cd7178a5 Mon Sep 17 00:00:00 2001 From: junwei Date: Wed, 4 Feb 2026 14:41:09 +0400 Subject: [PATCH 1/3] feat(ButtonGroup): support group component --- packages/vant/src/button/Button.tsx | 18 +- packages/vant/src/button/ButtonGroup.tsx | 39 ++++ packages/vant/src/button/README.md | 45 +++- packages/vant/src/button/README.zh-CN.md | 45 +++- packages/vant/src/button/demo/index.vue | 29 +++ packages/vant/src/button/index.less | 63 ++++++ packages/vant/src/button/index.ts | 6 + .../test/__snapshots__/demo-ssr.spec.ts.snap | 124 +++++++++++ .../test/__snapshots__/demo.spec.ts.snap | 102 +++++++++ ...index.spec.ts.snap => index.spec.tsx.snap} | 35 +++ packages/vant/src/button/test/index.spec.ts | 98 --------- packages/vant/src/button/test/index.spec.tsx | 204 ++++++++++++++++++ 12 files changed, 701 insertions(+), 107 deletions(-) create mode 100644 packages/vant/src/button/ButtonGroup.tsx rename packages/vant/src/button/test/__snapshots__/{index.spec.ts.snap => index.spec.tsx.snap} (61%) delete mode 100644 packages/vant/src/button/test/index.spec.ts create mode 100644 packages/vant/src/button/test/index.spec.tsx diff --git a/packages/vant/src/button/Button.tsx b/packages/vant/src/button/Button.tsx index d5de399dbc3..7c8d96e96dd 100644 --- a/packages/vant/src/button/Button.tsx +++ b/packages/vant/src/button/Button.tsx @@ -16,6 +16,9 @@ import { } from '../utils'; import { useRoute, routeProps } from '../composables/use-route'; +import { useParent } from '@vant/use'; +import { BUTTON_GROUP_KEY } from './ButtonGroup'; + // Components import { Icon } from '../icon'; import { Loading, LoadingType } from '../loading'; @@ -63,6 +66,7 @@ export default defineComponent({ setup(props, { emit, slots }) { const route = useRoute(); + const { parent: group } = useParent(BUTTON_GROUP_KEY); const renderLoadingIcon = () => { if (slots.loading) { @@ -146,10 +150,7 @@ export default defineComponent({ return () => { const { tag, - type, - size, block, - round, plain, square, loading, @@ -159,6 +160,17 @@ export default defineComponent({ iconPosition, } = props; + // Inherit from ButtonGroup if not explicitly set + const type = + props.type === 'default' && group?.props.type + ? group.props.type + : props.type; + const size = + props.size === 'normal' && group?.props.size + ? group.props.size + : props.size; + const round = props.round || group?.props.round; + const classes = [ bem([ type, diff --git a/packages/vant/src/button/ButtonGroup.tsx b/packages/vant/src/button/ButtonGroup.tsx new file mode 100644 index 00000000000..d93313a7faf --- /dev/null +++ b/packages/vant/src/button/ButtonGroup.tsx @@ -0,0 +1,39 @@ +import { + defineComponent, + type PropType, + type InjectionKey, + type ExtractPropTypes, +} from 'vue'; +import { createNamespace, makeStringProp } from '../utils'; +import { useChildren } from '@vant/use'; +import type { ButtonSize, ButtonType } from './types'; + +const [name, bem] = createNamespace('button-group'); + +export const buttonGroupProps = { + type: makeStringProp('primary'), + size: String as PropType, + round: Boolean, +}; + +export type ButtonGroupProps = ExtractPropTypes; + +export type ButtonGroupProvide = { + props: ButtonGroupProps; +}; + +export const BUTTON_GROUP_KEY: InjectionKey = Symbol(name); + +export default defineComponent({ + name, + + props: buttonGroupProps, + + setup(props, { slots }) { + const { linkChildren } = useChildren(BUTTON_GROUP_KEY); + + linkChildren({ props }); + + return () =>
{slots.default?.()}
; + }, +}); diff --git a/packages/vant/src/button/README.md b/packages/vant/src/button/README.md index 02f567f355f..cf77b93c458 100644 --- a/packages/vant/src/button/README.md +++ b/packages/vant/src/button/README.md @@ -132,6 +132,30 @@ Customize the button color using the `color` prop. ``` +### Button Group + +Use the `ButtonGroup` component to combine multiple buttons into a group. You can use `type`, `size`, `round` props to set the style of buttons in the group. + +```html + + Button 1 + Button 2 + Button 3 + + + + Button 1 + Button 2 + Button 3 + + + + Button 1 + Button 2 + Button 3 + +``` + ### Animated Button With the combination of the Button and [Swipe component](<(/#/en-US/swipe)>), you can create an animated button effect with vertical scrolling. @@ -160,7 +184,7 @@ With the combination of the Button and [Swipe component](<(/#/en-US/swipe)>), yo ## API -### Props +### Button Props | Attribute | Description | Type | Default | | --- | --- | --- | --- | @@ -186,14 +210,22 @@ With the combination of the Button and [Swipe component](<(/#/en-US/swipe)>), yo | to | The target route should navigate to when clicked on, same as the [to prop](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-to) of Vue Router | _string \| object_ | - | | replace | If true, the navigation will not leave a history record | _boolean_ | `false` | -### Events +### ButtonGroup Props + +| Attribute | Description | Type | Default | +| --- | --- | --- | --- | +| type | Can be set to `primary` `success` `warning` `danger` | _string_ | `primary` | +| size | Can be set to `large` `small` `mini` | _string_ | - | +| round | Whether to be round button | _boolean_ | `false` | + +### Button Events | Event | Description | Arguments | | --- | --- | --- | | click | Emitted when button is clicked and not disabled or loading | _event: MouseEvent_ | | touchstart | Emitted when button is touched | _event: TouchEvent_ | -### Slots +### Button Slots | Name | Description | | ------- | ------------------- | @@ -201,6 +233,12 @@ With the combination of the Button and [Swipe component](<(/#/en-US/swipe)>), yo | icon | Custom icon | | loading | Custom loading icon | +### ButtonGroup Slots + +| Name | Description | +| ------- | -------------------- | +| default | Button group content | + ### Types The component exports the following type definitions: @@ -210,6 +248,7 @@ import type { ButtonType, ButtonSize, ButtonProps, + ButtonGroupProps, ButtonNativeType, ButtonIconPosition, } from 'vant'; diff --git a/packages/vant/src/button/README.zh-CN.md b/packages/vant/src/button/README.zh-CN.md index ed400b32d8e..371321e27c2 100644 --- a/packages/vant/src/button/README.zh-CN.md +++ b/packages/vant/src/button/README.zh-CN.md @@ -132,6 +132,30 @@ app.use(Button); ``` +### 按钮组 + +通过 `ButtonGroup` 组件可以将多个按钮组合在一起,形成按钮组。可以通过 `type`、`size`、`round` 等\属性统一设置组内按钮的样式。 + +```html + + 按钮 1 + 按钮 2 + 按钮 3 + + + + 按钮 1 + 按钮 2 + 按钮 3 + + + + 按钮 1 + 按钮 2 + 按钮 3 + +``` + ### 动画按钮 搭配 Button 和 [Swipe 组件](/#/zh-CN/swipe),可以实现垂直滚动的动画按钮效果。 @@ -160,7 +184,7 @@ app.use(Button); ## API -### Props +### Button Props | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | @@ -187,14 +211,22 @@ app.use(Button); | to | 点击后跳转的目标路由对象,等同于 Vue Router 的 [to 属性](https://router.vuejs.org/zh/api/interfaces/RouterLinkProps.html#Properties-to) | _string \| object_ | - | | replace | 是否在跳转时替换当前页面历史 | _boolean_ | `false` | -### Events +### ButtonGroup Props + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| type | 类型,可选值为 `primary` `success` `warning` `danger` | _string_ | `primary` | +| size | 尺寸,可选值为 `large` `small` `mini` | _string_ | - | +| round | 是否为圆形按钮 | _boolean_ | `false` | + +### Button Events | 事件名 | 说明 | 回调参数 | | ---------- | ---------------------------------------- | ------------------- | | click | 点击按钮,且按钮状态不为加载或禁用时触发 | _event: MouseEvent_ | | touchstart | 开始触摸按钮时触发 | _event: TouchEvent_ | -### Slots +### Button Slots | 名称 | 说明 | | ------- | -------------- | @@ -202,6 +234,12 @@ app.use(Button); | icon | 自定义图标 | | loading | 自定义加载图标 | +### ButtonGroup Slots + +| 名称 | 说明 | +| ------- | ---------- | +| default | 按钮组内容 | + ### 类型定义 组件导出以下类型定义: @@ -211,6 +249,7 @@ import type { ButtonType, ButtonSize, ButtonProps, + ButtonGroupProps, ButtonNativeType, ButtonIconPosition, } from 'vant'; diff --git a/packages/vant/src/button/demo/index.vue b/packages/vant/src/button/demo/index.vue index 7c469f8ce7a..ec01b2c3d4d 100644 --- a/packages/vant/src/button/demo/index.vue +++ b/packages/vant/src/button/demo/index.vue @@ -1,5 +1,6 @@ @@ -149,6 +152,28 @@ const t = useTranslate({ /> + +
+ + {{ t('button') }} 1 + {{ t('button') }} 2 + {{ t('button') }} 3 + +
+
+ + {{ t('button') }} 1 + {{ t('button') }} 2 + {{ t('button') }} 3 + +
+ + {{ t('button') }} 1 + {{ t('button') }} 2 + {{ t('button') }} 3 + +
+ .van-button { + margin-right: 0; + } + .van-doc-demo-block { padding: 0 var(--van-padding-md); } diff --git a/packages/vant/src/button/index.less b/packages/vant/src/button/index.less index dde932d2cac..30c123b6e33 100644 --- a/packages/vant/src/button/index.less +++ b/packages/vant/src/button/index.less @@ -53,6 +53,7 @@ border-radius: var(--van-button-radius); cursor: pointer; transition: opacity var(--van-duration-fast); + appearance: none; -webkit-appearance: none; -webkit-font-smoothing: auto; @@ -241,3 +242,65 @@ } } } + +.van-button-group { + display: inline-flex; + + .van-button { + position: relative; + border-radius: 0; + + &:first-child { + border-top-left-radius: var(--van-button-radius); + border-bottom-left-radius: var(--van-button-radius); + } + + &:last-child { + border-top-right-radius: var(--van-button-radius); + border-bottom-right-radius: var(--van-button-radius); + } + + &:not(:first-child) { + border-left: 0; + box-shadow: -1px 0 0 0 rgba(255, 255, 255, 0.3); + } + + &--hairline { + &::after { + border-radius: 0; + } + + &:first-child::after { + border-top-left-radius: calc(var(--van-button-radius) * 2); + border-bottom-left-radius: calc(var(--van-button-radius) * 2); + } + + &:last-child::after { + border-top-right-radius: calc(var(--van-button-radius) * 2); + border-bottom-right-radius: calc(var(--van-button-radius) * 2); + } + } + + &--round { + &:first-child { + border-top-left-radius: var(--van-button-round-radius); + border-bottom-left-radius: var(--van-button-round-radius); + } + + &:last-child { + border-top-right-radius: var(--van-button-round-radius); + border-bottom-right-radius: var(--van-button-round-radius); + } + + &--hairline:first-child::after { + border-top-left-radius: var(--van-button-round-radius); + border-bottom-left-radius: var(--van-button-round-radius); + } + + &--hairline:last-child::after { + border-top-right-radius: var(--van-button-round-radius); + border-bottom-right-radius: var(--van-button-round-radius); + } + } + } +} diff --git a/packages/vant/src/button/index.ts b/packages/vant/src/button/index.ts index 0635a7e1775..985a3d5171d 100644 --- a/packages/vant/src/button/index.ts +++ b/packages/vant/src/button/index.ts @@ -1,10 +1,15 @@ import { withInstall } from '../utils'; import _Button from './Button'; +import _ButtonGroup from './ButtonGroup'; export const Button = withInstall(_Button); +export const ButtonGroup = withInstall(_ButtonGroup); export default Button; export { buttonProps } from './Button'; +export { buttonGroupProps, BUTTON_GROUP_KEY } from './ButtonGroup'; + export type { ButtonProps } from './Button'; +export type { ButtonGroupProps, ButtonGroupProvide } from './ButtonGroup'; export type { ButtonType, ButtonSize, @@ -16,5 +21,6 @@ export type { declare module 'vue' { export interface GlobalComponents { VanButton: typeof Button; + VanButtonGroup: typeof ButtonGroup; } } diff --git a/packages/vant/src/button/test/__snapshots__/demo-ssr.spec.ts.snap b/packages/vant/src/button/test/__snapshots__/demo-ssr.spec.ts.snap index d86063f9bb0..fb295a3c352 100644 --- a/packages/vant/src/button/test/__snapshots__/demo-ssr.spec.ts.snap +++ b/packages/vant/src/button/test/__snapshots__/demo-ssr.spec.ts.snap @@ -463,6 +463,130 @@ exports[`should render demo and match snapshot 1`] = ` +
+ +
+
+ + + + +
+
+
+
+ + + + +
+
+
+ + + + +
+
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+ + + +
+
+ + +
+`; + exports[`should render icon in the right side when setting icon-position to right 1`] = ` + + + + ); + }, + }); + expect(wrapper.html()).toMatchSnapshot(); +}); + +test('should render button group with different types', () => { + const wrapper = mount({ + render() { + return ( + + + + + + ); + }, + }); + expect(wrapper.find('.van-button-group').exists()).toBe(true); + expect(wrapper.findAll('.van-button').length).toBe(3); +}); + +test('should apply correct class to button group', () => { + const wrapper = mount(ButtonGroup); + expect(wrapper.classes()).toContain('van-button-group'); +}); + +test('should inherit type from button group', () => { + const wrapper = mount({ + render() { + return ( + + + + + ); + }, + }); + const buttons = wrapper.findAll('.van-button'); + expect(buttons[0].classes()).toContain('van-button--primary'); + expect(buttons[1].classes()).toContain('van-button--primary'); +}); + +test('should inherit size from button group', () => { + const wrapper = mount({ + render() { + return ( + + + + + ); + }, + }); + const buttons = wrapper.findAll('.van-button'); + expect(buttons[0].classes()).toContain('van-button--small'); + expect(buttons[1].classes()).toContain('van-button--small'); +}); + +test('should inherit round from button group', () => { + const wrapper = mount({ + render() { + return ( + + + + + ); + }, + }); + const buttons = wrapper.findAll('.van-button'); + expect(buttons[0].classes()).toContain('van-button--round'); + expect(buttons[1].classes()).toContain('van-button--round'); +}); + +test('button props should override group props', () => { + const wrapper = mount({ + render() { + return ( + + + + + ); + }, + }); + const buttons = wrapper.findAll('.van-button'); + // First button overrides group props + expect(buttons[0].classes()).toContain('van-button--danger'); + expect(buttons[0].classes()).toContain('van-button--mini'); + // Second button inherits from group + expect(buttons[1].classes()).toContain('van-button--primary'); + expect(buttons[1].classes()).toContain('van-button--small'); +}); From f20c24f47110470be9ebcc0a5419f7cbff09c46f Mon Sep 17 00:00:00 2001 From: junwei Date: Wed, 4 Feb 2026 14:49:23 +0400 Subject: [PATCH 2/3] chore: update --- packages/vant/src/button/README.zh-CN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vant/src/button/README.zh-CN.md b/packages/vant/src/button/README.zh-CN.md index 371321e27c2..e484853546a 100644 --- a/packages/vant/src/button/README.zh-CN.md +++ b/packages/vant/src/button/README.zh-CN.md @@ -134,7 +134,7 @@ app.use(Button); ### 按钮组 -通过 `ButtonGroup` 组件可以将多个按钮组合在一起,形成按钮组。可以通过 `type`、`size`、`round` 等\属性统一设置组内按钮的样式。 +通过 `ButtonGroup` 组件可以将多个按钮组合在一起,形成按钮组。可以通过 `type`、`size`、`round` 等属性统一设置组内按钮的样式。 ```html From e88800730fba510d19b1400156346ca29b4ae77a Mon Sep 17 00:00:00 2001 From: junwei Date: Wed, 4 Feb 2026 14:50:04 +0400 Subject: [PATCH 3/3] chore: udpate --- packages/vant/src/button/README.zh-CN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vant/src/button/README.zh-CN.md b/packages/vant/src/button/README.zh-CN.md index e484853546a..a904b521c9a 100644 --- a/packages/vant/src/button/README.zh-CN.md +++ b/packages/vant/src/button/README.zh-CN.md @@ -134,7 +134,7 @@ app.use(Button); ### 按钮组 -通过 `ButtonGroup` 组件可以将多个按钮组合在一起,形成按钮组。可以通过 `type`、`size`、`round` 等属性统一设置组内按钮的样式。 +通过 `ButtonGroup` 组件可以将多个按钮组合在一起,形成按钮组。可以通过 `type`、`size`、`round` 属性统一设置组内按钮的样式。 ```html