Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions packages/vant/src/button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -146,10 +150,7 @@ export default defineComponent({
return () => {
const {
tag,
type,
size,
block,
round,
plain,
square,
loading,
Expand All @@ -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;

Choose a reason for hiding this comment

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

high

The inheritance logic for the round prop has a potential bug. Since round is a Boolean prop, props.round will always be true or false. If a user explicitly sets :round="false" on a Button within a ButtonGroup that has round, the button will still inherit round from the group because false || group?.props.round would evaluate to group?.props.round (which is true). This prevents explicit false overrides.

To correctly handle explicit false values overriding parent props, you need to check if the prop was explicitly passed. A common pattern for this is to check vnode.props or adjust the prop definition to allow undefined as a default, then check for undefined.

For example, you could modify the buttonProps definition for round to allow undefined as a default, or use getCurrentInstance() to check instance.vnode.props.

Suggested change
const round = props.round || group?.props.round;
const round = props.round !== undefined && props.round !== null ? props.round : group?.props.round;

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

vue 的 Boolean prop 在未传时值是 false 不是 undefined 把,这样无法继承 group 属性了。
另外看 checker 中貌似是同样的问题,使用 getCurrentInstance 检查是否显式传入?


const classes = [
bem([
type,
Expand Down
39 changes: 39 additions & 0 deletions packages/vant/src/button/ButtonGroup.tsx
Original file line number Diff line number Diff line change
@@ -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<ButtonType>('primary'),
size: String as PropType<ButtonSize>,
round: Boolean,
};

export type ButtonGroupProps = ExtractPropTypes<typeof buttonGroupProps>;

export type ButtonGroupProvide = {
props: ButtonGroupProps;
};

export const BUTTON_GROUP_KEY: InjectionKey<ButtonGroupProvide> = Symbol(name);

export default defineComponent({
name,

props: buttonGroupProps,

setup(props, { slots }) {
const { linkChildren } = useChildren(BUTTON_GROUP_KEY);

linkChildren({ props });

return () => <div class={bem()}>{slots.default?.()}</div>;
},
});
45 changes: 42 additions & 3 deletions packages/vant/src/button/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,30 @@ Customize the button color using the `color` prop.
</van-button>
```

### 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
<van-button-group type="primary">
<van-button>Button 1</van-button>
<van-button>Button 2</van-button>
<van-button>Button 3</van-button>
</van-button-group>

<van-button-group size="small">
<van-button>Button 1</van-button>
<van-button>Button 2</van-button>
<van-button>Button 3</van-button>
</van-button-group>

<van-button-group round>
<van-button>Button 1</van-button>
<van-button>Button 2</van-button>
<van-button>Button 3</van-button>
</van-button-group>
```

### Animated Button

With the combination of the Button and [Swipe component](<(/#/en-US/swipe)>), you can create an animated button effect with vertical scrolling.
Expand Down Expand Up @@ -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 |
| --- | --- | --- | --- |
Expand All @@ -186,21 +210,35 @@ 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 |
| ------- | ------------------- |
| default | Default slot |
| icon | Custom icon |
| loading | Custom loading icon |

### ButtonGroup Slots

| Name | Description |
| ------- | -------------------- |
| default | Button group content |

### Types

The component exports the following type definitions:
Expand All @@ -210,6 +248,7 @@ import type {
ButtonType,
ButtonSize,
ButtonProps,
ButtonGroupProps,
ButtonNativeType,
ButtonIconPosition,
} from 'vant';
Expand Down
45 changes: 42 additions & 3 deletions packages/vant/src/button/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,30 @@ app.use(Button);
</van-button>
```

### 按钮组

通过 `ButtonGroup` 组件可以将多个按钮组合在一起,形成按钮组。可以通过 `type`、`size`、`round` 属性统一设置组内按钮的样式。

```html
<van-button-group type="primary">
<van-button>按钮 1</van-button>
<van-button>按钮 2</van-button>
<van-button>按钮 3</van-button>
</van-button-group>

<van-button-group size="small">
<van-button>按钮 1</van-button>
<van-button>按钮 2</van-button>
<van-button>按钮 3</van-button>
</van-button-group>

<van-button-group round>
<van-button>按钮 1</van-button>
<van-button>按钮 2</van-button>
<van-button>按钮 3</van-button>
</van-button-group>
```

### 动画按钮

搭配 Button 和 [Swipe 组件](/#/zh-CN/swipe),可以实现垂直滚动的动画按钮效果。
Expand Down Expand Up @@ -160,7 +184,7 @@ app.use(Button);

## API

### Props
### Button Props

| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
Expand All @@ -187,21 +211,35 @@ 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

| 名称 | 说明 |
| ------- | -------------- |
| default | 按钮内容 |
| icon | 自定义图标 |
| loading | 自定义加载图标 |

### ButtonGroup Slots

| 名称 | 说明 |
| ------- | ---------- |
| default | 按钮组内容 |

### 类型定义

组件导出以下类型定义:
Expand All @@ -211,6 +249,7 @@ import type {
ButtonType,
ButtonSize,
ButtonProps,
ButtonGroupProps,
ButtonNativeType,
ButtonIconPosition,
} from 'vant';
Expand Down
29 changes: 29 additions & 0 deletions packages/vant/src/button/demo/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import VanButton from '..';
import { ButtonGroup as VanButtonGroup } from '..';
import VanSwipe from '../../swipe';
import VanSwipeItem from '../../swipe-item';
import { cdnURL, useTranslate } from '../../../docs/site';
Expand Down Expand Up @@ -36,6 +37,7 @@ const t = useTranslate({
animatedButton: '动画按钮',
doTask: '做任务',
lottery: '抽大奖',
buttonGroup: '按钮组',
},
'en-US': {
type: 'Type',
Expand Down Expand Up @@ -68,6 +70,7 @@ const t = useTranslate({
animatedButton: 'Animated Button',
doTask: 'Do Task',
lottery: 'Lottery',
buttonGroup: 'Button Group',
},
});
</script>
Expand Down Expand Up @@ -149,6 +152,28 @@ const t = useTranslate({
/>
</demo-block>

<demo-block :title="t('buttonGroup')">
<div class="demo-button-row">
<van-button-group type="primary">
<van-button>{{ t('button') }} 1</van-button>
<van-button>{{ t('button') }} 2</van-button>
<van-button>{{ t('button') }} 3</van-button>
</van-button-group>
</div>
<div class="demo-button-row">
<van-button-group size="small">
<van-button>{{ t('button') }} 1</van-button>
<van-button>{{ t('button') }} 2</van-button>
<van-button>{{ t('button') }} 3</van-button>
</van-button-group>
</div>
<van-button-group round>
<van-button>{{ t('button') }} 1</van-button>
<van-button>{{ t('button') }} 2</van-button>
<van-button>{{ t('button') }} 3</van-button>
</van-button-group>
</demo-block>

<demo-block :title="t('animatedButton')">
<van-button type="danger" round>
<van-swipe
Expand Down Expand Up @@ -178,6 +203,10 @@ const t = useTranslate({
}
}

.van-button-group > .van-button {
margin-right: 0;
}

.van-doc-demo-block {
padding: 0 var(--van-padding-md);
}
Expand Down
63 changes: 63 additions & 0 deletions packages/vant/src/button/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
}
}
}
Loading
Loading