diff --git a/src-next/button/README.md b/src-next/button/README.md
new file mode 100644
index 000000000..3414b3b30
--- /dev/null
+++ b/src-next/button/README.md
@@ -0,0 +1,134 @@
+# Button
+
+### Install
+
+```js
+import Vue from 'vue';
+import { Button } from 'vant';
+
+Vue.use(Button);
+```
+
+## Usage
+
+### Type
+
+```html
+Default
+Primary
+Info
+Danger
+Warning
+```
+
+### Plain
+
+```html
+Primary
+Danger
+```
+
+### Hairline
+
+```html
+Hairline
+Hairline
+```
+
+### Disabled
+
+```html
+Diabled
+Diabled
+```
+
+### Loading
+
+```html
+
+
+
+```
+
+### Shape
+
+```html
+Square
+Round
+```
+
+### Icon
+
+```html
+
+Button
+Button
+```
+
+### Size
+
+```html
+Large
+Normal
+Small
+Mini
+```
+
+### Block Element
+
+```html
+Block Element
+```
+
+### Route
+
+```html
+URL
+Vue Router
+```
+
+### Custom Color
+
+```html
+Pure
+Pure
+Gradient
+```
+
+## API
+
+### Props
+
+| Attribute | Description | Type | Default |
+| --- | --- | --- | --- |
+| type | Can be set to `primary` `info` `warning` `danger` | _string_ | `default` |
+| size | Can be set to `large` `small` `mini` | _string_ | `normal` |
+| text | Text | _string_ | - |
+| color `v2.1.8` | Color, support linear-gradient | _string_ | - |
+| icon | Left Icon | _string_ | - |
+| icon-prefix `v2.6.0` | Icon className prefix | _string_ | `van-icon` |
+| tag | HTML Tag | _string_ | `button` |
+| native-type | Native Type Attribute | _string_ | `''` |
+| plain | Whether to be plain button | _boolean_ | `false` |
+| block | Whether to set display block | _boolean_ | `false` |
+| round | Whether to be round button | _boolean_ | `false` |
+| square | Whether to be square button | _boolean_ | `false` |
+| disabled | Whether to disable button | _boolean_ | `false` |
+| loading | Whether show loading status | _boolean_ | `false` |
+| loading-text | Loading text | _string_ | - |
+| loading-type | Loading type, can be set to `spinner` | _string_ | `circular` |
+| loading-size | Loading icon size | _string_ | `20px` |
+| url | Link URL | _string_ | - |
+| to | Target route of the link, same as to of vue-router | _string \| object_ | - |
+| replace | If true, the navigation will not leave a history record | _boolean_ | `false` |
+
+### Events
+
+| Event | Description | Arguments |
+| --- | --- | --- |
+| click | Triggered when click button and not disabled or loading | _event: Event_ |
+| touchstart | Triggered when touch start | _event: TouchEvent_ |
diff --git a/src-next/button/README.zh-CN.md b/src-next/button/README.zh-CN.md
new file mode 100644
index 000000000..092bd1211
--- /dev/null
+++ b/src-next/button/README.zh-CN.md
@@ -0,0 +1,157 @@
+# Button 按钮
+
+### 引入
+
+```js
+import Vue from 'vue';
+import { Button } from 'vant';
+
+Vue.use(Button);
+```
+
+## 代码演示
+
+### 按钮类型
+
+支持`default`、`primary`、`info`、`warning`、`danger`五种类型,默认为`default`
+
+```html
+默认按钮
+主要按钮
+信息按钮
+警告按钮
+危险按钮
+```
+
+### 朴素按钮
+
+通过`plain`属性将按钮设置为朴素按钮,朴素按钮的文字为按钮颜色,背景为白色。
+
+```html
+朴素按钮
+朴素按钮
+```
+
+### 细边框
+
+设置`hairline`属性可以开启 0.5px 边框,基于伪类实现
+
+```html
+细边框按钮
+细边框按钮
+```
+
+### 禁用状态
+
+通过`disabled`属性来禁用按钮,禁用状态下按钮不可点击
+
+```html
+禁用状态
+禁用状态
+```
+
+### 加载状态
+
+通过`loading`属性设置按钮为加载状态,加载状态下默认会隐藏按钮文字,可以通过`loading-text`设置加载状态下的文字
+
+```html
+
+
+
+```
+
+### 按钮形状
+
+通过`square`设置方形按钮,通过`round`设置圆形按钮
+
+```html
+方形按钮
+圆形按钮
+```
+
+### 图标按钮
+
+通过`icon`属性设置按钮图标,支持 Icon 组件里的所有图标,也可以传入图标 URL
+
+```html
+
+按钮
+按钮
+```
+
+### 按钮尺寸
+
+支持`large`、`normal`、`small`、`mini`四种尺寸,默认为`normal`
+
+```html
+大号按钮
+普通按钮
+小型按钮
+迷你按钮
+```
+
+### 块级元素
+
+按钮在默认情况下为行内块级元素,通过`block`属性可以将按钮的元素类型设置为块级元素
+
+```html
+块级元素
+```
+
+### 页面导航
+
+可以通过`url`属性进行 URL 跳转,或通过`to`属性进行路由跳转
+
+```html
+URL 跳转
+路由跳转
+```
+
+### 自定义颜色
+
+通过`color`属性可以自定义按钮的颜色
+
+```html
+单色按钮
+单色按钮
+渐变色按钮
+```
+
+## API
+
+### Props
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| type | 类型,可选值为 `primary` `info` `warning` `danger` | _string_ | `default` |
+| size | 尺寸,可选值为 `large` `small` `mini` | _string_ | `normal` |
+| text | 按钮文字 | _string_ | - |
+| color `v2.1.8` | 按钮颜色,支持传入`linear-gradient`渐变色 | _string_ | - |
+| icon | 左侧[图标名称](#/zh-CN/icon)或图片链接 | _string_ | - |
+| icon-prefix `v2.6.0` | 图标类名前缀,同 Icon 组件的 [class-prefix 属性](#/zh-CN/icon#props) | _string_ | `van-icon` |
+| tag | 根节点的 HTML 标签 | _string_ | `button` |
+| native-type | 原生 button 标签的 type 属性 | _string_ | - |
+| block | 是否为块级元素 | _boolean_ | `false` |
+| plain | 是否为朴素按钮 | _boolean_ | `false` |
+| square | 是否为方形按钮 | _boolean_ | `false` |
+| round | 是否为圆形按钮 | _boolean_ | `false` |
+| disabled | 是否禁用按钮 | _boolean_ | `false` |
+| hairline | 是否使用 0.5px 边框 | _boolean_ | `false` |
+| loading | 是否显示为加载状态 | _boolean_ | `false` |
+| loading-text | 加载状态提示文字 | _string_ | - |
+| loading-type | [加载图标类型](#/zh-CN/loading),可选值为`spinner` | _string_ | `circular` |
+| loading-size | 加载图标大小 | _string_ | `20px` |
+| url | 点击后跳转的链接地址 | _string_ | - |
+| to | 点击后跳转的目标路由对象,同 vue-router 的 [to 属性](https://router.vuejs.org/zh/api/#to) | _string \| object_ | - |
+| replace | 是否在跳转时替换当前页面历史 | _boolean_ | `false` |
+
+### Events
+
+| 事件名 | 说明 | 回调参数 |
+| ---------- | ---------------------------------------- | ------------------- |
+| click | 点击按钮,且按钮状态不为加载或禁用时触发 | _event: Event_ |
+| touchstart | 开始触摸按钮时触发 | _event: TouchEvent_ |
diff --git a/src-next/button/demo/index.vue b/src-next/button/demo/index.vue
new file mode 100644
index 000000000..2eb52e3bd
--- /dev/null
+++ b/src-next/button/demo/index.vue
@@ -0,0 +1,173 @@
+
+
+
+
+ {{ t('default') }}
+ {{ t('primary') }}
+ {{ t('info') }}
+
+ {{ t('danger') }}
+ {{ t('warning') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('large') }}
+ {{ t('normal') }}
+ {{ t('small') }}
+ {{ t('mini') }}
+
+
+
+ {{ t('blockElement') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src-next/button/index.js b/src-next/button/index.js
new file mode 100644
index 000000000..689b9aaa1
--- /dev/null
+++ b/src-next/button/index.js
@@ -0,0 +1,147 @@
+// Utils
+import { createNamespace } from '../utils';
+import { BORDER_SURROUND, WHITE } from '../utils/constant';
+import { routeProps, route } from '../utils/router';
+
+// Components
+import Icon from '../icon';
+import Loading from '../loading';
+
+const [createComponent, bem] = createNamespace('button');
+
+export default createComponent({
+ props: {
+ ...routeProps,
+ text: String,
+ icon: String,
+ color: String,
+ block: Boolean,
+ plain: Boolean,
+ round: Boolean,
+ square: Boolean,
+ loading: Boolean,
+ hairline: Boolean,
+ disabled: Boolean,
+ iconPrefix: String,
+ nativeType: String,
+ loadingText: String,
+ loadingType: String,
+ tag: {
+ type: String,
+ default: 'button',
+ },
+ type: {
+ type: String,
+ default: 'default',
+ },
+ size: {
+ type: String,
+ default: 'normal',
+ },
+ loadingSize: {
+ type: String,
+ default: '20px',
+ },
+ },
+
+ methods: {
+ onClick() {
+ if (!this.loading && !this.disabled) {
+ this.$emit('click', event);
+ route(this.$router, this);
+ }
+ },
+
+ onTouchstart(event) {
+ this.$emit('touchstart', event);
+ },
+
+ genContent() {
+ const Content = [];
+
+ if (this.loading) {
+ Content.push(
+
+ );
+ } else if (this.icon) {
+ Content.push(
+
+ );
+ }
+
+ let text;
+ if (this.loading) {
+ text = this.loadingText;
+ } else {
+ text = this.$slots.default ? this.$slots.default() : this.text;
+ }
+
+ if (text) {
+ Content.push({text});
+ }
+
+ return Content;
+ },
+ },
+
+ render() {
+ const { tag, type, color, plain, disabled, loading, hairline } = this;
+
+ const style = {};
+
+ if (color) {
+ style.color = plain ? color : WHITE;
+
+ if (!plain) {
+ // Use background instead of backgroundColor to make linear-gradient work
+ style.background = color;
+ }
+
+ // hide border when color is linear-gradient
+ if (color.indexOf('gradient') !== -1) {
+ style.border = 0;
+ } else {
+ style.borderColor = color;
+ }
+ }
+
+ const classes = [
+ bem([
+ type,
+ this.size,
+ {
+ plain,
+ loading,
+ disabled,
+ hairline,
+ block: this.block,
+ round: this.round,
+ square: this.square,
+ },
+ ]),
+ { [BORDER_SURROUND]: hairline },
+ ];
+
+ return (
+
+ {this.genContent()}
+
+ );
+ },
+});
diff --git a/src-next/button/index.less b/src-next/button/index.less
new file mode 100644
index 000000000..5f2ad00ae
--- /dev/null
+++ b/src-next/button/index.less
@@ -0,0 +1,183 @@
+@import '../style/var';
+
+.van-button {
+ position: relative;
+ display: inline-block;
+ box-sizing: border-box;
+ height: @button-default-height;
+ margin: 0;
+ padding: 0;
+ font-size: @button-default-font-size;
+ line-height: @button-default-line-height;
+ text-align: center;
+ border-radius: @button-border-radius;
+ cursor: pointer;
+ transition: opacity @animation-duration-fast;
+ -webkit-appearance: none;
+
+ &::before {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 100%;
+ height: 100%;
+ background-color: @black;
+ border: inherit;
+ border-color: @black;
+ border-radius: inherit; /* inherit parent's border radius */
+ transform: translate(-50%, -50%);
+ opacity: 0;
+ content: ' ';
+ }
+
+ &:active::before {
+ opacity: 0.1;
+ }
+
+ &--loading,
+ &--disabled {
+ &::before {
+ display: none;
+ }
+ }
+
+ &--default {
+ color: @button-default-color;
+ background-color: @button-default-background-color;
+ border: @button-border-width solid @button-default-border-color;
+ }
+
+ &--primary {
+ color: @button-primary-color;
+ background-color: @button-primary-background-color;
+ border: @button-border-width solid @button-primary-border-color;
+ }
+
+ &--info {
+ color: @button-info-color;
+ background-color: @button-info-background-color;
+ border: @button-border-width solid @button-info-border-color;
+ }
+
+ &--danger {
+ color: @button-danger-color;
+ background-color: @button-danger-background-color;
+ border: @button-border-width solid @button-danger-border-color;
+ }
+
+ &--warning {
+ color: @button-warning-color;
+ background-color: @button-warning-background-color;
+ border: @button-border-width solid @button-warning-border-color;
+ }
+
+ &--plain {
+ background-color: @button-plain-background-color;
+
+ &.van-button--primary {
+ color: @button-primary-background-color;
+ }
+
+ &.van-button--info {
+ color: @button-info-background-color;
+ }
+
+ &.van-button--danger {
+ color: @button-danger-background-color;
+ }
+
+ &.van-button--warning {
+ color: @button-warning-background-color;
+ }
+ }
+
+ &--large {
+ width: 100%;
+ height: @button-large-height;
+ }
+
+ &--normal {
+ padding: 0 15px;
+ font-size: @button-normal-font-size;
+ }
+
+ &--small {
+ height: @button-small-height;
+ padding: 0 @padding-xs;
+ font-size: @button-small-font-size;
+ }
+
+ &__loading {
+ color: inherit;
+ font-size: inherit;
+ }
+
+ &--mini {
+ height: @button-mini-height;
+ padding: 0 @padding-base;
+ font-size: @button-mini-font-size;
+
+ & + .van-button--mini {
+ margin-left: @padding-base;
+ }
+ }
+
+ &--block {
+ display: block;
+ width: 100%;
+ }
+
+ &--disabled {
+ cursor: not-allowed;
+ opacity: @button-disabled-opacity;
+ }
+
+ &--loading {
+ cursor: default;
+ }
+
+ &--round {
+ border-radius: @button-round-border-radius;
+ }
+
+ &--square {
+ border-radius: 0;
+ }
+
+ // align-items are ignored when flex container is a button in legacy safari
+ // see: https://bugs.webkit.org/show_bug.cgi?id=169700
+ &__content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ }
+
+ &__icon {
+ min-width: 1em;
+ font-size: 1.2em;
+ line-height: inherit;
+ }
+
+ &__icon + &__text,
+ &__loading + &__text {
+ margin-left: 5px;
+ }
+
+ &--hairline {
+ border-width: 0;
+
+ &::after {
+ border-color: inherit;
+ border-radius: @button-border-radius * 2;
+ }
+
+ &.van-button--round::after {
+ border-radius: @button-round-border-radius;
+ }
+
+ &.van-button--square::after {
+ border-radius: 0;
+ }
+ }
+}
diff --git a/src-next/button/test/__snapshots__/demo.spec.js.snap b/src-next/button/test/__snapshots__/demo.spec.js.snap
new file mode 100644
index 000000000..9d218df71
--- /dev/null
+++ b/src-next/button/test/__snapshots__/demo.spec.js.snap
@@ -0,0 +1,86 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders demo correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src-next/button/test/__snapshots__/index.spec.js.snap b/src-next/button/test/__snapshots__/index.spec.js.snap
new file mode 100644
index 000000000..239b474c2
--- /dev/null
+++ b/src-next/button/test/__snapshots__/index.spec.js.snap
@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`icon-prefix prop 1`] = `
+
+`;
+
+exports[`loading-size prop 1`] = `
+
+`;
diff --git a/src-next/button/test/demo.spec.js b/src-next/button/test/demo.spec.js
new file mode 100644
index 000000000..5c70922b5
--- /dev/null
+++ b/src-next/button/test/demo.spec.js
@@ -0,0 +1,4 @@
+import Demo from '../demo';
+import { snapshotDemo } from '../../../test/demo';
+
+snapshotDemo(Demo);
diff --git a/src-next/button/test/index.spec.js b/src-next/button/test/index.spec.js
new file mode 100644
index 000000000..bd9d96bdb
--- /dev/null
+++ b/src-next/button/test/index.spec.js
@@ -0,0 +1,95 @@
+import { mount } from '../../../test';
+import Button from '..';
+
+test('loading-size prop', () => {
+ const wrapper = mount(Button, {
+ propsData: {
+ loading: true,
+ loadingSize: '10px',
+ },
+ });
+ expect(wrapper).toMatchSnapshot();
+});
+
+test('click event', () => {
+ const onClick = jest.fn();
+ const wrapper = mount(Button, {
+ context: {
+ on: {
+ click: onClick,
+ },
+ },
+ });
+
+ wrapper.trigger('click');
+ expect(onClick).toHaveBeenCalled();
+});
+
+test('not trigger click event when disabled', () => {
+ const onClick = jest.fn();
+ const wrapper = mount(Button, {
+ propsData: {
+ disabled: true,
+ },
+ context: {
+ on: {
+ click: onClick,
+ },
+ },
+ });
+
+ wrapper.trigger('click');
+ expect(onClick).toHaveBeenCalledTimes(0);
+});
+
+test('not trigger click event when loading', () => {
+ const onClick = jest.fn();
+ const wrapper = mount(Button, {
+ propsData: {
+ loading: true,
+ },
+ context: {
+ on: {
+ click: onClick,
+ },
+ },
+ });
+
+ wrapper.trigger('click');
+ expect(onClick).toHaveBeenCalledTimes(0);
+});
+
+test('touchstart event', () => {
+ const onTouchstart = jest.fn();
+ const wrapper = mount(Button, {
+ context: {
+ on: {
+ touchstart: onTouchstart,
+ },
+ },
+ });
+
+ wrapper.trigger('touchstart');
+ expect(onTouchstart).toHaveBeenCalled();
+});
+
+test('hide border when color is gradient', () => {
+ const wrapper = mount(Button, {
+ propsData: {
+ color: 'linear-gradient(#000, #fff)',
+ },
+ });
+
+ expect(wrapper.element.style.border).toEqual('0px');
+});
+
+test('icon-prefix prop', () => {
+ const wrapper = mount(Button, {
+ propsData: {
+ icon: 'success',
+ iconPrefix: 'my-icon',
+ },
+ });
+
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/src-next/utils/router.ts b/src-next/utils/router.ts
index 7a005e4e9..7e822dc31 100644
--- a/src-next/utils/router.ts
+++ b/src-next/utils/router.ts
@@ -2,12 +2,11 @@
* Vue Router support
*/
-import { RenderContext } from 'vue/types';
-import VueRouter, { RawLocation } from 'vue-router/types';
+import type { Router, RouteLocation } from 'vue-router';
export type RouteConfig = {
url?: string;
- to?: RawLocation;
+ to?: RouteLocation;
replace?: boolean;
};
@@ -19,7 +18,7 @@ function isRedundantNavigation(err: Error) {
);
}
-export function route(router: VueRouter, config: RouteConfig) {
+export function route(router: Router, config: RouteConfig) {
const { to, url, replace } = config;
if (to && router) {
const promise = router[replace ? 'replace' : 'push'](to);
@@ -37,14 +36,10 @@ export function route(router: VueRouter, config: RouteConfig) {
}
}
-export function functionalRoute(context: RenderContext) {
- route(context.parent && context.parent.$router, context.props);
-}
-
export type RouteProps = {
url?: string;
replace?: boolean;
- to?: RawLocation;
+ to?: RouteLocation;
};
export const routeProps = {
diff --git a/vant.config.js b/vant.config.js
index 585ec7160..e2f7a50d8 100644
--- a/vant.config.js
+++ b/vant.config.js
@@ -78,10 +78,10 @@ module.exports = {
{
title: '基础组件',
items: [
- // {
- // path: 'button',
- // title: 'Button 按钮',
- // },
+ {
+ path: 'button',
+ title: 'Button 按钮',
+ },
// {
// path: 'cell',
// title: 'Cell 单元格',
@@ -425,10 +425,10 @@ module.exports = {
{
title: 'Basic Components',
items: [
- // {
- // path: 'button',
- // title: 'Button',
- // },
+ {
+ path: 'button',
+ title: 'Button',
+ },
// {
// path: 'cell',
// title: 'Cell',