diff --git a/docs/markdown/v2-progress-tracking.md b/docs/markdown/v2-progress-tracking.md
index 91506ddc9..181a168f1 100644
--- a/docs/markdown/v2-progress-tracking.md
+++ b/docs/markdown/v2-progress-tracking.md
@@ -52,6 +52,7 @@
## 新特性
- 新增`Skeleton`骨架屏组件
+- 新增`DropdownMenu`下拉菜单组件
### ActionSheet
diff --git a/docs/src/demo-entry.js b/docs/src/demo-entry.js
index b69130510..167bc55f8 100644
--- a/docs/src/demo-entry.js
+++ b/docs/src/demo-entry.js
@@ -18,6 +18,7 @@ export default {
'coupon-list': () => wrapper(import('../../packages/coupon-list/demo'), 'coupon-list'),
'datetime-picker': () => wrapper(import('../../packages/datetime-picker/demo'), 'datetime-picker'),
'dialog': () => wrapper(import('../../packages/dialog/demo'), 'dialog'),
+ 'dropdown-menu': () => wrapper(import('../../packages/dropdown-menu/demo'), 'dropdown-menu'),
'field': () => wrapper(import('../../packages/field/demo'), 'field'),
'goods-action': () => wrapper(import('../../packages/goods-action/demo'), 'goods-action'),
'icon': () => wrapper(import('../../packages/icon/demo'), 'icon'),
diff --git a/docs/src/doc.config.js b/docs/src/doc.config.js
index ac339f471..f416f57f8 100644
--- a/docs/src/doc.config.js
+++ b/docs/src/doc.config.js
@@ -175,6 +175,10 @@ module.exports = {
path: '/dialog',
title: 'Dialog 弹出框'
},
+ {
+ path: '/dropdown-menu',
+ title: 'DropdownMenu 下拉菜单'
+ },
{
path: '/loading',
title: 'Loading 加载'
@@ -486,6 +490,10 @@ module.exports = {
path: '/dialog',
title: 'Dialog'
},
+ {
+ path: '/dropdown-menu',
+ title: 'DropdownMenu'
+ },
{
path: '/loading',
title: 'Loading'
diff --git a/docs/src/docs-entry.js b/docs/src/docs-entry.js
index ba4bb3c31..d3a8d2fff 100644
--- a/docs/src/docs-entry.js
+++ b/docs/src/docs-entry.js
@@ -43,6 +43,8 @@ export default {
'datetime-picker.zh-CN': () => import('../../packages/datetime-picker/zh-CN.md'),
'dialog.en-US': () => import('../../packages/dialog/en-US.md'),
'dialog.zh-CN': () => import('../../packages/dialog/zh-CN.md'),
+ 'dropdown-menu.en-US': () => import('../../packages/dropdown-menu/en-US.md'),
+ 'dropdown-menu.zh-CN': () => import('../../packages/dropdown-menu/zh-CN.md'),
'field.en-US': () => import('../../packages/field/en-US.md'),
'field.zh-CN': () => import('../../packages/field/zh-CN.md'),
'goods-action.en-US': () => import('../../packages/goods-action/en-US.md'),
diff --git a/packages/dropdown-item/index.js b/packages/dropdown-item/index.js
new file mode 100644
index 000000000..97a09ac47
--- /dev/null
+++ b/packages/dropdown-item/index.js
@@ -0,0 +1,85 @@
+import { use, isDef } from '../utils';
+import Cell from '../cell';
+import Icon from '../icon';
+import Popup from '../popup';
+
+const [sfc, bem] = use('dropdown-item');
+
+export default sfc({
+ props: {
+ value: null,
+ title: String,
+ options: Array
+ },
+
+ inject: ['vanDropdownMenu'],
+
+ data() {
+ return {
+ show: false
+ };
+ },
+
+ created() {
+ const { items } = this.vanDropdownMenu;
+ const index = this.vanDropdownMenu.slots().indexOf(this.$vnode);
+ items.splice(index === -1 ? items.length : index, 0, this);
+ },
+
+ beforeDestroy() {
+ this.vanDropdownMenu.items = this.vanDropdownMenu.items.filter(item => item !== this);
+ },
+
+ computed: {
+ displayTitle() {
+ if (this.title) {
+ return this.title;
+ }
+
+ const match = this.options.filter(option => option.value === this.value);
+ return match.length ? match[0].text : '';
+ }
+ },
+
+ methods: {
+ toggle(show) {
+ this.show = isDef(show) ? show : !this.show;
+ }
+ },
+
+ render(h) {
+ const { top, zIndex, activeColor } = this.vanDropdownMenu;
+
+ const Options = this.options.map(option => {
+ const active = option.value === this.value;
+ return (
+ {
+ this.show = false;
+ this.$emit('input', option.value);
+ }}
+ >
+ {active && }
+ |
+ );
+ });
+
+ return (
+
+
+ {Options}
+ {this.slots('default')}
+
+
+ );
+ }
+});
diff --git a/packages/dropdown-item/index.less b/packages/dropdown-item/index.less
new file mode 100644
index 000000000..b93652881
--- /dev/null
+++ b/packages/dropdown-item/index.less
@@ -0,0 +1,18 @@
+@import '../style/var';
+
+.van-dropdown-item {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ overflow: hidden;
+
+ &__content {
+ position: absolute;
+ }
+
+ &__icon {
+ display: block;
+ line-height: inherit;
+ }
+}
diff --git a/packages/dropdown-menu/demo/index.vue b/packages/dropdown-menu/demo/index.vue
new file mode 100644
index 000000000..9396eceda
--- /dev/null
+++ b/packages/dropdown-menu/demo/index.vue
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('confirm') }}
+
+
+
+
+
+
+
+
diff --git a/packages/dropdown-menu/en-US.md b/packages/dropdown-menu/en-US.md
new file mode 100644
index 000000000..9df7da7dc
--- /dev/null
+++ b/packages/dropdown-menu/en-US.md
@@ -0,0 +1,100 @@
+## DropdownMenu
+
+### Install
+
+``` javascript
+import { DropdownMenu, DropdownItem } from 'vant';
+
+Vue.use(DropdownMenu).use(DropdownItem);
+```
+
+### Usage
+
+#### Basic Usage
+
+```html
+
+
+
+
+```
+
+```js
+export default {
+ data() {
+ return {
+ value1: 0,
+ value2: 'a',
+ option1: [
+ { text: 'Option1', value: 0 },
+ { text: 'Option2', value: 1 },
+ { text: 'Option3', value: 2 }
+ ],
+ option2: [
+ { text: 'Option A', value: 'a' },
+ { text: 'Option B', value: 'b' },
+ { text: 'Option C', value: 'c' },
+ ]
+ }
+ }
+};
+```
+
+### Custom Content
+
+```html
+
+
+
+
+
+ Confirm
+
+
+```
+
+```js
+export default {
+ data() {
+ return {
+ value: 0,
+ switch1: false,
+ switch2: false,
+ option: [
+ { text: 'Option1', value: 0 },
+ { text: 'Option2', value: 1 },
+ { text: 'Option3', value: 2 }
+ ]
+ }
+ },
+
+ methods: {
+ onConfirm() {
+ this.$refs.item.toggle();
+ }
+ }
+};
+```
+
+### DropdownMenu Props
+
+| Attribute | Description | Type | Default |
+|------|------|------|------|------|
+| active-color | Active color of title and option | `String` | `#1989fa` |
+| z-index | z-index of menu item | `Number` | `10` |
+
+### DropdownItem Props
+
+| Attribute | Description | Type | Default |
+|------|------|------|------|------|
+| value | Value of current option,can use `v-model` | `String | Number` | - |
+| title | Item title | `String` | Text of selected option |
+| options | Options | `Array` | `[]` |
+
+### DropdownItem Methods
+
+Use ref to get DropdownItem instance and call instance methods
+
+| Name | Attribute | Return value | Description |
+|------|------|------|------|
+| toggle | show: boolean | - | Toggle display |
diff --git a/packages/dropdown-menu/index.js b/packages/dropdown-menu/index.js
new file mode 100644
index 000000000..4f2761f34
--- /dev/null
+++ b/packages/dropdown-menu/index.js
@@ -0,0 +1,71 @@
+import { use } from '../utils';
+import { BLUE } from '../utils/color';
+
+const [sfc, bem] = use('dropdown-menu');
+
+export default sfc({
+ props: {
+ zIndex: {
+ type: Number,
+ default: 10
+ },
+ activeColor: {
+ type: String,
+ default: BLUE
+ }
+ },
+
+ provide() {
+ return {
+ vanDropdownMenu: this
+ };
+ },
+
+ data() {
+ return {
+ top: 0,
+ items: []
+ };
+ },
+
+ methods: {
+ toggleItem(active) {
+ const { menu } = this.$refs;
+ const rect = menu.getBoundingClientRect();
+ this.top = rect.y + rect.height;
+
+ this.items.forEach((item, index) => {
+ if (index === active) {
+ item.toggle();
+ } else {
+ item.toggle(false);
+ }
+ });
+ }
+ },
+
+ render(h) {
+ const Titles = this.items.map((item, index) => (
+ {
+ this.toggleItem(index);
+ }}
+ >
+
+ {item.displayTitle}
+
+
+ ));
+
+ return (
+
+ {Titles}
+ {this.slots('default')}
+
+ );
+ }
+});
diff --git a/packages/dropdown-menu/index.less b/packages/dropdown-menu/index.less
new file mode 100644
index 000000000..30c3b65b3
--- /dev/null
+++ b/packages/dropdown-menu/index.less
@@ -0,0 +1,43 @@
+@import '../style/var';
+
+.van-dropdown-menu {
+ display: flex;
+ height: 50px;
+ user-select: none;
+ background-color: @white;
+
+ &__item {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:active {
+ opacity: .7;
+ }
+ }
+
+ &__title {
+ font-size: 15px;
+ position: relative;
+
+ &::after {
+ position: absolute;
+ content: '';
+ top: 3px;
+ right: -12px;
+ opacity: .6;
+ border: 3px solid;
+ transform: rotate(-45deg);
+ border-color: transparent transparent currentColor currentColor;
+ }
+
+ &--active {
+ &::after {
+ top: 7px;
+ opacity: 1;
+ transform: rotate(135deg);
+ }
+ }
+ }
+}
diff --git a/packages/dropdown-menu/test/__snapshots__/demo.spec.js.snap b/packages/dropdown-menu/test/__snapshots__/demo.spec.js.snap
new file mode 100644
index 000000000..e0832281f
--- /dev/null
+++ b/packages/dropdown-menu/test/__snapshots__/demo.spec.js.snap
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders demo correctly 1`] = `
+
+`;
diff --git a/packages/dropdown-menu/test/__snapshots__/index.spec.js.snap b/packages/dropdown-menu/test/__snapshots__/index.spec.js.snap
new file mode 100644
index 000000000..61b19c988
--- /dev/null
+++ b/packages/dropdown-menu/test/__snapshots__/index.spec.js.snap
@@ -0,0 +1,137 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`click option 1`] = `
+
+`;
+
+exports[`destroy one item 1`] = `
+
+`;
+
+exports[`didn\`t find matched option 1`] = `
+
+`;
+
+exports[`show dropdown item 1`] = `
+
+`;
+
+exports[`show dropdown item 2`] = `
+
+`;
+
+exports[`show dropdown item 3`] = `
+
+`;
+
+exports[`title prop 1`] = `
+
+`;
diff --git a/packages/dropdown-menu/test/demo.spec.js b/packages/dropdown-menu/test/demo.spec.js
new file mode 100644
index 000000000..d647cfabc
--- /dev/null
+++ b/packages/dropdown-menu/test/demo.spec.js
@@ -0,0 +1,4 @@
+import Demo from '../demo';
+import demoTest from '../../../test/demo-test';
+
+demoTest(Demo);
diff --git a/packages/dropdown-menu/test/index.spec.js b/packages/dropdown-menu/test/index.spec.js
new file mode 100644
index 000000000..9a517478b
--- /dev/null
+++ b/packages/dropdown-menu/test/index.spec.js
@@ -0,0 +1,100 @@
+import { mount, later } from '../../../test/utils';
+import DropdownMenu from '..';
+import DropdownItem from '../../dropdown-item';
+
+function renderWrapper(options = {}) {
+ return mount({
+ template: `
+
+
+
+
+ `,
+ components: {
+ DropdownItem,
+ DropdownMenu
+ },
+ data() {
+ return {
+ value: options.value || 0,
+ title: options.title || '',
+ options: [
+ { text: 'A', value: 0 },
+ { text: 'B', value: 1 }
+ ]
+ };
+ }
+ });
+}
+
+test('show dropdown item', async () => {
+ const wrapper = renderWrapper();
+
+ await later();
+
+ const titles = wrapper.findAll('.van-dropdown-menu__title');
+
+ titles.at(0).trigger('click');
+ expect(wrapper).toMatchSnapshot();
+
+ titles.at(0).trigger('click');
+ expect(wrapper).toMatchSnapshot();
+
+ titles.at(1).trigger('click');
+ expect(wrapper).toMatchSnapshot();
+});
+
+test('click option', async () => {
+ const wrapper = renderWrapper();
+
+ await later();
+
+ const titles = wrapper.findAll('.van-dropdown-menu__title');
+ titles.at(0).trigger('click');
+
+ const options = wrapper.findAll('.van-dropdown-item .van-cell');
+
+ options.at(1).trigger('click');
+ expect(wrapper).toMatchSnapshot();
+});
+
+test('title prop', async () => {
+ const wrapper = renderWrapper({ title: 'Title' });
+ await later();
+ expect(wrapper).toMatchSnapshot();
+});
+
+test('didn`t find matched option', async () => {
+ const wrapper = renderWrapper({ value: -1 });
+ await later();
+ expect(wrapper).toMatchSnapshot();
+});
+
+test('destroy one item', async () => {
+ const wrapper = mount({
+ template: `
+
+
+
+
+ `,
+ components: {
+ DropdownItem,
+ DropdownMenu
+ },
+ data() {
+ return {
+ value: 0,
+ render: true,
+ options: [
+ { text: 'A', value: 0 },
+ { text: 'B', value: 1 }
+ ]
+ };
+ }
+ });
+
+ await later();
+ wrapper.setData({ render: false });
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/packages/dropdown-menu/zh-CN.md b/packages/dropdown-menu/zh-CN.md
new file mode 100644
index 000000000..c628e71ab
--- /dev/null
+++ b/packages/dropdown-menu/zh-CN.md
@@ -0,0 +1,102 @@
+## DropdownMenu 下拉菜单
+
+### 使用指南
+
+``` javascript
+import { DropdownMenu, DropdownItem } from 'vant';
+
+Vue.use(DropdownMenu).use(DropdownItem);
+```
+
+### 代码演示
+
+#### 基础用法
+
+```html
+
+
+
+
+```
+
+```js
+export default {
+ data() {
+ return {
+ value1: 0,
+ value2: 'a',
+ option1: [
+ { text: '全部商品', value: 0 },
+ { text: '新款商品', value: 1 },
+ { text: '活动商品', value: 2 }
+ ],
+ option2: [
+ { text: '默认排序', value: 'a' },
+ { text: '好评排序', value: 'b' },
+ { text: '销量排序', value: 'c' },
+ ]
+ }
+ }
+};
+```
+
+### 自定义菜单内容
+
+通过插槽可以自定义`DropdownItem`的内容,此时需要使用实例上的`toggle`方法手动控制菜单的显示
+
+```html
+
+
+
+
+
+ 确认
+
+
+```
+
+```js
+export default {
+ data() {
+ return {
+ value: 0,
+ switch1: false,
+ switch2: false,
+ option: [
+ { text: '全部商品', value: 0 },
+ { text: '新款商品', value: 1 },
+ { text: '活动商品', value: 2 }
+ ]
+ }
+ },
+
+ methods: {
+ onConfirm() {
+ this.$refs.item.toggle();
+ }
+ }
+};
+```
+
+### DropdownMenu Props
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+|------|------|------|------|------|
+| active-color | 菜单标题和选项的选中态颜色 | `String` | `#1989fa` | - |
+| z-index | 菜单栏 z-index 层级 | `Number` | `10` | - |
+
+### DropdownItem Props
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+|------|------|------|------|------|
+| value | 当前选中项对应的 value,可以通过`v-model`双向绑定 | `String | Number` | - | - |
+| title | 菜单项标题 | `String` | 当前选中项文字 | - |
+| options | 选项数组 | `Array` | `[]` | - |
+
+### DropdownItem 方法
+
+通过 ref 可以获取到 DropdownItem 实例并调用实例方法
+
+| 方法名 | 参数 | 返回值 | 介绍 |
+|------|------|------|------|
+| toggle | show: boolean | - | 切换菜单是否展示 |
diff --git a/packages/index.less b/packages/index.less
index d902afd5e..b069e1e0c 100644
--- a/packages/index.less
+++ b/packages/index.less
@@ -47,6 +47,8 @@
/* action components */
@import './action-sheet/index';
@import './dialog/index';
+@import './dropdown-item/index';
+@import './dropdown-menu/index';
@import './picker/index';
@import './pull-refresh/index';
@import './notify/index';
diff --git a/packages/index.ts b/packages/index.ts
index 09def9ab6..3694875e7 100644
--- a/packages/index.ts
+++ b/packages/index.ts
@@ -23,6 +23,8 @@ import CouponCell from './coupon-cell';
import CouponList from './coupon-list';
import DatetimePicker from './datetime-picker';
import Dialog from './dialog';
+import DropdownItem from './dropdown-item';
+import DropdownMenu from './dropdown-menu';
import Field from './field';
import GoodsAction from './goods-action';
import GoodsActionButton from './goods-action-button';
@@ -104,6 +106,8 @@ const components = [
CouponList,
DatetimePicker,
Dialog,
+ DropdownItem,
+ DropdownMenu,
Field,
GoodsAction,
GoodsActionButton,
@@ -190,6 +194,8 @@ export {
CouponList,
DatetimePicker,
Dialog,
+ DropdownItem,
+ DropdownMenu,
Field,
GoodsAction,
GoodsActionButton,
diff --git a/packages/skeleton/en-US.md b/packages/skeleton/en-US.md
index 468c2f371..68a115f0c 100644
--- a/packages/skeleton/en-US.md
+++ b/packages/skeleton/en-US.md
@@ -47,6 +47,7 @@ export default {
}
};
```
+
### Props
| Attribute | Description | Type | Default |