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 @@ + + + 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`] = ` +
+
B
+
B
+ + +
+`; + +exports[`destroy one item 1`] = ` +
+
A
+ + +
+`; + +exports[`didn\`t find matched option 1`] = ` +
+
+
+ + +
+`; + +exports[`show dropdown item 1`] = ` +
+
A
+
A
+
+
+
+
A
+
+
+
+
+
B
+
+
+
+
+ +
+`; + +exports[`show dropdown item 2`] = ` +
+
A
+
A
+ + +
+`; + +exports[`show dropdown item 3`] = ` +
+
A
+
A
+ +
+
+
+
A
+
+
+
+
+
B
+
+
+
+
+
+`; + +exports[`title prop 1`] = ` +
+
Title
+
Title
+ + +
+`; 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 |