diff --git a/docs/markdown/theme.zh-CN.md b/docs/markdown/theme.zh-CN.md
index 2287b65bb..bc62ba87f 100644
--- a/docs/markdown/theme.zh-CN.md
+++ b/docs/markdown/theme.zh-CN.md
@@ -75,7 +75,7 @@ module.exports = {
{
loader: 'less-loader',
options: {
- // 若使用 less-loader@5,请移除 lessOptions 这一级,直接配置选项。
+ // 若 less-loader 版本小于 6.0,请移除 lessOptions 这一级,直接配置选项。
lessOptions: {
modifyVars: {
// 直接覆盖变量
@@ -101,7 +101,7 @@ module.exports = {
css: {
loaderOptions: {
less: {
- // 若使用 less-loader@5,请移除 lessOptions 这一级,直接配置选项。
+ // 若 less-loader 版本小于 6.0,请移除 lessOptions 这一级,直接配置选项。
lessOptions: {
modifyVars: {
// 直接覆盖变量
diff --git a/package.json b/package.json
index 0c422cc63..83427ac82 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
"license": "MIT",
"dependencies": {
"@babel/runtime": "7.x",
+ "@popperjs/core": "^2.5.4",
"@vant/icons": "1.4.0",
"@vant/use": "^0.1.0",
"vue-lazyload": "1.2.3"
diff --git a/src/popover/README.md b/src/popover/README.md
new file mode 100644
index 000000000..4d9499ece
--- /dev/null
+++ b/src/popover/README.md
@@ -0,0 +1,195 @@
+# Popover
+
+### Install
+
+```js
+import Vue from 'vue';
+import { Popover } from 'vant';
+
+Vue.use(Popover);
+```
+
+## Usage
+
+### Basic Usage
+
+```html
+
+
+
+ Light Theme
+
+
+
+```
+
+```js
+import { Toast } from 'vant';
+
+export default {
+ data() {
+ return {
+ showPopover: false,
+ actions: [
+ { text: 'Option 1' },
+ { text: 'Option 2' },
+ { text: 'Option 3' },
+ ],
+ };
+ },
+ methods: {
+ onSelect(action) {
+ Toast(action.text);
+ },
+ },
+};
+```
+
+### Dark theme
+
+Using the `theme` prop to change the style of Popover.
+
+```html
+
+
+
+ Dark Theme
+
+
+
+```
+
+```js
+export default {
+ data() {
+ return {
+ showPopover: false,
+ actions: [
+ { text: 'Option 1' },
+ { text: 'Option 2' },
+ { text: 'Option 3' },
+ ],
+ };
+ },
+};
+```
+
+### Placement
+
+```html
+
+```
+
+`placement` supports the following values:
+
+```bash
+top # Top middle
+top-start # Top left
+top-end # Top right
+left # Left middle
+left-start # Left top
+left-end # Left bottom
+right # Right middle
+right-start # Right top
+right-end # Right bottom
+bottom # Bottom middle
+bottom-start # Bottom left
+bottom-end # Bottom right
+```
+
+### Show Icon
+
+```html
+
+
+
+ Show Icon
+
+
+
+```
+
+```js
+export default {
+ data() {
+ return {
+ showPopover: false,
+ actions: [
+ { text: 'Option 1', icon: 'add-o' },
+ { text: 'Option 2', icon: 'music-o' },
+ { text: 'Option 3', icon: 'more-o' },
+ ],
+ };
+ },
+};
+```
+
+### Disable Action
+
+Using the `disabled` option to disable an action.
+
+```html
+
+
+
+ Disable Action
+
+
+
+```
+
+```js
+export default {
+ data() {
+ return {
+ showPopover: false,
+ actions: [
+ { text: 'Option 1', disabled: true },
+ { text: 'Option 2', disabled: true },
+ { text: 'Option 3' },
+ ],
+ };
+ },
+};
+```
+
+## API
+
+### Props
+
+| Attribute | Description | Type | Default |
+| --- | --- | --- | --- |
+| v-model | Whether to show Popover | _boolean_ | `false` |
+| actions | Actions | _Action[]_ | `[]` |
+| placement | Placement | _string_ | `bottom` |
+| theme | Theme,can be set to `dark` | _string_ | `light` |
+| overlay | Whether to show overlay | _boolean_ | `false` |
+| close-on-click-action | Whether to close when clicking action | _boolean_ | `true` |
+| close-on-click-outside | Whether to close when clicking outside | _boolean_ | `true` |
+| get-container | Return the mount node for Popover | _string \| () => Element_ | `body` |
+
+### Data Structure of Action
+
+| Key | Description | Type |
+| --------- | ----------------------- | --------- |
+| text | Action Text | _string_ |
+| icon | Icon | _string_ |
+| disabled | Whether to be disabled | _boolean_ |
+| className | className of the option | _any_ |
+
+### Events
+
+| Event | Description | Arguments |
+| ------ | -------------------------------- | ------------------------------- |
+| select | Triggered when clicking action | _action: Action, index: number_ |
+| open | Triggered when opening Popover | - |
+| close | Triggered when closing Popover | - |
+| opened | Triggered when Popover is opened | - |
+| closed | Triggered when Popover is closed | - |
+
+### Slots
+
+| Name | Description |
+| --------- | ----------------- |
+| default | Custom content |
+| reference | Reference Element |
diff --git a/src/popover/README.zh-CN.md b/src/popover/README.zh-CN.md
new file mode 100644
index 000000000..12ade4c26
--- /dev/null
+++ b/src/popover/README.zh-CN.md
@@ -0,0 +1,239 @@
+# Popover 气泡弹出框
+
+### 介绍
+
+弹出式的气泡菜单。
+
+### 引入
+
+```js
+import Vue from 'vue';
+import { Popover } from 'vant';
+
+Vue.use(Popover);
+```
+
+## 代码演示
+
+### 基础用法
+
+当 Popover 弹出时,会基于 `reference` 插槽的内容进行定位。
+
+```html
+
+
+
+ 浅色风格
+
+
+
+```
+
+```js
+import { Toast } from 'vant';
+
+export default {
+ data() {
+ return {
+ showPopover: false,
+ // 通过 actions 属性来定义菜单选项
+ actions: [{ text: '选项一' }, { text: '选项二' }, { text: '选项三' }],
+ };
+ },
+ methods: {
+ onSelect(action) {
+ Toast(action.text);
+ },
+ },
+};
+```
+
+### 深色风格
+
+Popover 支持浅色和深色两种风格,默认为浅色风格,将 `theme` 属性设置为 `dark` 可切换为深色风格。
+
+```html
+
+
+
+ 深色风格
+
+
+
+```
+
+```js
+export default {
+ data() {
+ return {
+ showPopover: false,
+ actions: [{ text: '选项一' }, { text: '选项二' }, { text: '选项三' }],
+ };
+ },
+};
+```
+
+### 弹出位置
+
+通过 `placement` 属性来控制气泡的弹出位置。
+
+```html
+
+```
+
+`placement` 支持以下值:
+
+```bash
+top # 顶部中间位置
+top-start # 顶部左侧位置
+top-end # 顶部右侧位置
+left # 左侧中间位置
+left-start # 左侧上方位置
+left-end # 左侧下方位置
+right # 右侧中间位置
+right-start # 右侧上方位置
+right-end # 右侧下方位置
+bottom # 底部中间位置
+bottom-start # 底部左侧位置
+bottom-end # 底部右侧位置
+```
+
+### 展示图标
+
+在 `actions` 数组中,可以通过 `icon` 字段来定义选项的图标,支持传入[图标名称](#/zh-CN/icon)或图片链接。
+
+```html
+
+
+
+ 展示图标
+
+
+
+```
+
+```js
+export default {
+ data() {
+ return {
+ showPopover: false,
+ actions: [
+ { text: '选项一', icon: 'add-o' },
+ { text: '选项二', icon: 'music-o' },
+ { text: '选项三', icon: 'more-o' },
+ ],
+ };
+ },
+};
+```
+
+### 禁用选项
+
+在 `actions` 数组中,可以通过 `disabled` 字段来禁用某个选项。
+
+```html
+
+
+
+ 禁用选项
+
+
+
+```
+
+```js
+export default {
+ data() {
+ return {
+ showPopover: false,
+ actions: [
+ { text: '选项一', disabled: true },
+ { text: '选项二', disabled: true },
+ { text: '选项三' },
+ ],
+ };
+ },
+};
+```
+
+### 自定义内容
+
+通过默认插槽,可以在 Popover 内部放置任意内容。
+
+```html
+
+
+
+
+
+
+ 自定义内容
+
+
+
+```
+
+```js
+export default {
+ data() {
+ return {
+ showPopover: false,
+ };
+ },
+};
+```
+
+## API
+
+### Props
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| v-model | 是否展示气泡弹出层 | _boolean_ | `false` |
+| actions | 选项列表 | _Action[]_ | `[]` |
+| placement | 弹出位置 | _string_ | `bottom` |
+| theme | 主题风格,可选值为 `dark` | _string_ | `light` |
+| overlay | 是否显示遮罩层 | _boolean_ | `false` |
+| close-on-click-action | 是否在点击选项后关闭 | _boolean_ | `true` |
+| close-on-click-outside | 是否在点击外部元素后关闭菜单 | _boolean_ | `true` |
+| get-container | 指定挂载的节点,[用法示例](#/zh-CN/popup#zhi-ding-gua-zai-wei-zhi) | _string \| () => Element_ | `body` |
+
+### Action 数据结构
+
+`actions` 属性是一个由对象构成的数组,数组中的每个对象配置一列,对象可以包含以下值:
+
+| 键名 | 说明 | 类型 |
+| --- | --- | --- |
+| text | 选项文字 | _string_ |
+| icon | 文字左侧的图标,支持传入[图标名称](#/zh-CN/icon)或图片链接 | _string_ |
+| disabled | 是否为禁用状态 | _boolean_ |
+| className | 为对应选项添加额外的类名 | _any_ |
+
+### Events
+
+| 事件名 | 说明 | 回调参数 |
+| ------ | ------------------------ | ------------------------------- |
+| select | 点击选项时触发 | _action: Action, index: number_ |
+| open | 打开菜单时触发 | - |
+| close | 关闭菜单时触发 | - |
+| opened | 打开菜单且动画结束后触发 | - |
+| closed | 关闭菜单且动画结束后触发 | - |
+
+### Slots
+
+| 名称 | 说明 |
+| --------- | --------------------------- |
+| default | 自定义菜单内容 |
+| reference | 触发 Popover 显示的元素内容 |
diff --git a/src/popover/demo/index.vue b/src/popover/demo/index.vue
new file mode 100644
index 000000000..b23669027
--- /dev/null
+++ b/src/popover/demo/index.vue
@@ -0,0 +1,249 @@
+
+
+
+
+
+
+ {{ t('lightTheme') }}
+
+
+
+
+
+
+ {{ t('darkTheme') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('showIcon') }}
+
+
+
+
+
+
+
+ {{ t('disableAction') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('customContent') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/popover/index.js b/src/popover/index.js
new file mode 100644
index 000000000..943fda9f9
--- /dev/null
+++ b/src/popover/index.js
@@ -0,0 +1,201 @@
+import { createPopper } from '@popperjs/core/lib/popper-lite';
+import offsetModifier from '@popperjs/core/lib/modifiers/offset';
+import extendsHelper from '@babel/runtime/helpers/esm/extends';
+import { createNamespace } from '../utils';
+import { BORDER_BOTTOM } from '../utils/constant';
+
+// Mixins
+import { ClickOutsideMixin } from '../mixins/click-outside';
+
+// Components
+import Icon from '../icon';
+import Popup from '../popup';
+
+// add Object.assign polyfill for popper.js
+// see: https://popper.js.org/docs/v2/browser-support/
+/* istanbul ignore if */
+if (!Object.assign) {
+ Object.assign = extendsHelper;
+}
+
+const [createComponent, bem] = createNamespace('popover');
+
+export default createComponent({
+ mixins: [
+ ClickOutsideMixin({
+ event: 'touchstart',
+ method: 'onClickOutside',
+ }),
+ ],
+
+ props: {
+ value: Boolean,
+ overlay: Boolean,
+ textColor: String,
+ backgroundColor: String,
+ offset: {
+ type: Array,
+ default: () => [0, 8],
+ },
+ theme: {
+ type: String,
+ default: 'light',
+ },
+ actions: {
+ type: Array,
+ default: () => [],
+ },
+ placement: {
+ type: String,
+ default: 'bottom',
+ },
+ getContainer: {
+ type: [String, Function],
+ default: 'body',
+ },
+ closeOnClickAction: {
+ type: Boolean,
+ default: true,
+ },
+ },
+
+ watch: {
+ value: 'updateLocation',
+ placement: 'updateLocation',
+ },
+
+ mounted() {
+ this.updateLocation();
+ },
+
+ beforeDestroy() {
+ if (this.popper) {
+ this.popper.destroy();
+ this.popper = null;
+ }
+ },
+
+ methods: {
+ createPopper() {
+ return createPopper(this.$refs.wrapper, this.$refs.popover.$el, {
+ placement: this.placement,
+ modifiers: [
+ {
+ name: 'computeStyles',
+ options: {
+ adaptive: false,
+ gpuAcceleration: false,
+ },
+ },
+ {
+ ...offsetModifier,
+ options: {
+ offset: this.offset,
+ },
+ },
+ ],
+ });
+ },
+
+ updateLocation() {
+ this.$nextTick(() => {
+ if (!this.value) {
+ return;
+ }
+
+ if (!this.popper) {
+ this.popper = this.createPopper();
+ } else {
+ this.popper.setOptions({
+ placement: this.placement,
+ });
+ }
+ });
+ },
+
+ renderAction(action, index) {
+ const { icon, text, disabled, className } = action;
+ return (
+
this.onClickAction(action, index)}
+ >
+ {icon &&
}
+
{text}
+
+ );
+ },
+
+ onToggle(value) {
+ this.$emit('input', value);
+ },
+
+ onTouchstart(event) {
+ event.stopPropagation();
+ this.$emit('touchstart', event);
+ },
+
+ onClickAction(action, index) {
+ if (action.disabled) {
+ return;
+ }
+
+ this.$emit('select', action, index);
+
+ if (this.closeOnClickAction) {
+ this.$emit('input', false);
+ }
+ },
+
+ onClickOutside() {
+ this.$emit('input', false);
+ },
+
+ onOpen() {
+ this.$emit('open');
+ },
+
+ /* istanbul ignore next */
+ onOpened() {
+ this.$emit('opened');
+ },
+
+ onClose() {
+ this.$emit('close');
+ },
+
+ /* istanbul ignore next */
+ onClosed() {
+ this.$emit('closed');
+ },
+ },
+
+ render() {
+ return (
+
+
+
+
+ {this.slots('default') || this.actions.map(this.renderAction)}
+
+
+ {this.slots('reference')}
+
+ );
+ },
+});
diff --git a/src/popover/index.less b/src/popover/index.less
new file mode 100644
index 000000000..a97e0ec6a
--- /dev/null
+++ b/src/popover/index.less
@@ -0,0 +1,269 @@
+@import '../style/var';
+@import '../style/mixins/hairline';
+
+.van-popover {
+ position: absolute;
+ overflow: visible;
+ background-color: transparent;
+ transition: opacity 0.15s, transform 0.15s;
+
+ &__wrapper {
+ display: inline-block;
+ }
+
+ &__arrow {
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ border-width: @popover-arrow-size;
+ }
+
+ &__content {
+ overflow: hidden;
+ border-radius: @popover-border-radius;
+ }
+
+ &__action {
+ position: relative;
+ display: flex;
+ align-items: center;
+ box-sizing: border-box;
+ width: @popover-action-width;
+ height: @popover-action-height;
+ padding: 0 @padding-md;
+ font-size: @popover-action-font-size;
+ line-height: @line-height-md;
+
+ &:last-child {
+ .van-popover__action-text::after {
+ display: none;
+ }
+ }
+
+ &-text {
+ display: flex;
+ flex: 1;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ }
+
+ &-icon {
+ margin-right: @padding-xs;
+ font-size: @popover-action-icon-size;
+ }
+
+ &--with-icon {
+ .van-popover__action-text {
+ justify-content: flex-start;
+ }
+ }
+ }
+
+ &[data-popper-placement^='top'] {
+ .van-popover__arrow {
+ bottom: 0;
+ border-top-color: currentColor;
+ border-bottom-width: 0;
+ transform: translate(-50%, 100%);
+ }
+ }
+
+ &[data-popper-placement='top'] {
+ transform-origin: 50% 100%;
+
+ .van-popover__arrow {
+ left: 50%;
+ }
+ }
+
+ &[data-popper-placement='top-start'] {
+ transform-origin: 0 100%;
+
+ .van-popover__arrow {
+ left: @padding-md;
+ }
+ }
+
+ &[data-popper-placement='top-end'] {
+ transform-origin: 100% 100%;
+
+ .van-popover__arrow {
+ right: @padding-md;
+ }
+ }
+
+ &[data-popper-placement^='left'] {
+ .van-popover__arrow {
+ right: 0;
+ border-right-width: 0;
+ border-left-color: currentColor;
+ transform: translate(100%, -50%);
+ }
+ }
+
+ &[data-popper-placement='left'] {
+ transform-origin: 100% 50%;
+
+ .van-popover__arrow {
+ top: 50%;
+ }
+ }
+
+ &[data-popper-placement='left-start'] {
+ transform-origin: 100% 0;
+
+ .van-popover__arrow {
+ top: @padding-md;
+ }
+ }
+
+ &[data-popper-placement='left-end'] {
+ transform-origin: 100% 100%;
+
+ .van-popover__arrow {
+ bottom: @padding-md;
+ }
+ }
+
+ &[data-popper-placement^='right'] {
+ .van-popover__arrow {
+ left: 0;
+ border-right-color: currentColor;
+ border-left-width: 0;
+ transform: translate(-100%, -50%);
+ }
+ }
+
+ &[data-popper-placement='right'] {
+ transform-origin: 0 50%;
+
+ .van-popover__arrow {
+ top: 50%;
+ }
+ }
+
+ &[data-popper-placement='right-start'] {
+ transform-origin: 0 0;
+
+ .van-popover__arrow {
+ top: @padding-md;
+ }
+ }
+
+ &[data-popper-placement='right-end'] {
+ transform-origin: 0 100%;
+
+ .van-popover__arrow {
+ bottom: @padding-md;
+ }
+ }
+
+ &[data-popper-placement^='bottom'] {
+ .van-popover__arrow {
+ top: 0;
+ border-top-width: 0;
+ border-bottom-color: currentColor;
+ transform: translate(-50%, -100%);
+ }
+ }
+
+ &[data-popper-placement='bottom'] {
+ transform-origin: 50% 0;
+
+ .van-popover__arrow {
+ left: 50%;
+ }
+ }
+
+ &[data-popper-placement='bottom-start'] {
+ transform-origin: 0 0;
+
+ .van-popover__arrow {
+ left: @padding-md;
+ }
+ }
+
+ &[data-popper-placement='bottom-end'] {
+ transform-origin: 100% 0;
+
+ .van-popover__arrow {
+ right: @padding-md;
+ }
+ }
+
+ &--light {
+ color: @popover-light-text-color;
+
+ .van-popover__content {
+ background-color: @popover-light-background-color;
+ box-shadow: 0 2px 12px rgba(50, 50, 51, 0.12);
+ }
+
+ .van-popover__arrow {
+ color: @popover-light-background-color;
+ }
+
+ .van-popover__action {
+ &:active {
+ background-color: @active-color;
+ }
+
+ &--disabled {
+ color: @popover-light-action-disabled-text-color;
+
+ &:active {
+ background-color: transparent;
+ }
+ }
+ }
+ }
+
+ &--dark {
+ color: @popover-dark-text-color;
+
+ .van-popover__content {
+ background-color: @popover-dark-background-color;
+ }
+
+ .van-popover__arrow {
+ color: @popover-dark-background-color;
+ }
+
+ .van-popover__action {
+ &:active {
+ background-color: rgba(0, 0, 0, 0.2);
+ }
+
+ &--disabled {
+ color: @popover-dark-action-disabled-text-color;
+
+ &:active {
+ background-color: transparent;
+ }
+ }
+ }
+
+ .van-popover__action-text {
+ &::after {
+ border-color: @gray-7;
+ }
+ }
+ }
+
+ &-zoom-enter,
+ &-zoom-leave-active {
+ transform: scale(0.8);
+ opacity: 0;
+ }
+
+ &-zoom-enter-active {
+ transition-timing-function: @animation-timing-function-enter;
+ }
+
+ &-zoom-leave-active {
+ transition-timing-function: @animation-timing-function-leave;
+ }
+}
diff --git a/src/popover/test/__snapshots__/demo.spec.js.snap b/src/popover/test/__snapshots__/demo.spec.js.snap
new file mode 100644
index 000000000..643cfa927
--- /dev/null
+++ b/src/popover/test/__snapshots__/demo.spec.js.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders demo correctly 1`] = `
+
+`;
diff --git a/src/popover/test/__snapshots__/index.spec.js.snap b/src/popover/test/__snapshots__/index.spec.js.snap
new file mode 100644
index 000000000..5cb3f0706
--- /dev/null
+++ b/src/popover/test/__snapshots__/index.spec.js.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should allow to custom the className of action 1`] = `
+
+`;
+
+exports[`should locate to reference element when showed 1`] = ``;
+
+exports[`should locate to reference element when showed 2`] = `
+
+`;
+
+exports[`should locate to reference element when showed 3`] = `
+
+`;
+
+exports[`should watch placement prop and update location 1`] = `
+
+`;
diff --git a/src/popover/test/demo.spec.js b/src/popover/test/demo.spec.js
new file mode 100644
index 000000000..5c70922b5
--- /dev/null
+++ b/src/popover/test/demo.spec.js
@@ -0,0 +1,4 @@
+import Demo from '../demo';
+import { snapshotDemo } from '../../../test/demo';
+
+snapshotDemo(Demo);
diff --git a/src/popover/test/index.spec.js b/src/popover/test/index.spec.js
new file mode 100644
index 000000000..3a64d5e6d
--- /dev/null
+++ b/src/popover/test/index.spec.js
@@ -0,0 +1,131 @@
+import Popover from '..';
+import { later, mount, trigger } from '../../../test';
+
+const baseActions = [
+ { text: 'Option 1' },
+ { text: 'Option 2' },
+ { text: 'Option 3' },
+];
+
+test('should emit select event when clicking the action', () => {
+ const wrapper = mount(Popover, {
+ propsData: {
+ value: true,
+ actions: baseActions,
+ },
+ });
+
+ wrapper.find('.van-popover__action').trigger('click');
+ expect(wrapper.emitted('select')[0]).toEqual([baseActions[0], 0]);
+});
+
+test('should not emit select event when the action is disabled', () => {
+ const wrapper = mount(Popover, {
+ propsData: {
+ value: true,
+ actions: [{ text: 'Option', disabled: true }],
+ },
+ });
+
+ wrapper.find('.van-popover__action').trigger('click');
+ expect(wrapper.emitted('select')).toBeFalsy();
+});
+
+test('should close popover when clicking the action', () => {
+ const wrapper = mount(Popover, {
+ propsData: {
+ value: true,
+ actions: baseActions,
+ },
+ });
+
+ wrapper.find('.van-popover__action').trigger('click');
+ expect(wrapper.emitted('input')[0][0]).toEqual(false);
+
+ wrapper.setProps({ closeOnClickAction: false });
+ wrapper.find('.van-popover__action').trigger('click');
+ expect(wrapper.emitted('input').length).toEqual(1);
+});
+
+test('should allow to custom the className of action', () => {
+ const wrapper = mount(Popover, {
+ propsData: {
+ value: true,
+ actions: [{ text: 'Option', className: 'foo' }],
+ },
+ });
+
+ expect(wrapper.find('.van-popover__action').html()).toMatchSnapshot();
+});
+
+test('should not init popper.js instance before showed', async () => {
+ const wrapper = mount(Popover);
+ expect(wrapper.vm.popper).toBeFalsy();
+ wrapper.destroy();
+});
+
+test('should destroy popper.js instance when unmouted', async () => {
+ const wrapper = mount(Popover, {
+ propsData: {
+ value: true,
+ },
+ });
+
+ await later();
+ expect(wrapper.vm.popper).toBeTruthy();
+ wrapper.destroy();
+ expect(wrapper.vm.popper).toEqual(null);
+});
+
+test('should locate to reference element when showed', async () => {
+ const root = document.createElement('div');
+ const wrapper = mount(Popover, {
+ propsData: {
+ getContainer: () => root,
+ },
+ });
+
+ expect(root.innerHTML).toMatchSnapshot();
+
+ wrapper.setProps({ value: true });
+ await later();
+ expect(root.innerHTML).toMatchSnapshot();
+
+ wrapper.setProps({ value: false });
+ expect(root.innerHTML).toMatchSnapshot();
+});
+
+test('should watch placement prop and update location', async () => {
+ const root = document.createElement('div');
+ const wrapper = mount(Popover, {
+ propsData: {
+ value: true,
+ getContainer: () => root,
+ },
+ });
+
+ wrapper.setProps({
+ placement: 'top',
+ });
+
+ await later();
+ expect(root.innerHTML).toMatchSnapshot();
+});
+
+test('should close popover when touch outside content', async () => {
+ const root = document.createElement('div');
+ const wrapper = mount(Popover, {
+ propsData: {
+ value: true,
+ getContainer: () => root,
+ },
+ });
+
+ const popover = root.querySelector('.van-popover');
+ trigger(popover, 'touchstart');
+ expect(wrapper.emitted('input')).toBeFalsy();
+
+ document.body.appendChild(root);
+ trigger(document.body, 'touchstart');
+ expect(wrapper.emitted('input')[0][0]).toEqual(false);
+});
diff --git a/src/popup/index.less b/src/popup/index.less
index 88eec745b..cf9dddeb2 100644
--- a/src/popup/index.less
+++ b/src/popup/index.less
@@ -72,14 +72,14 @@
&-slide-left-enter-active,
&-slide-right-enter-active,
&-slide-bottom-enter-active {
- transition-timing-function: ease-out;
+ transition-timing-function: @animation-timing-function-enter;
}
&-slide-top-leave-active,
&-slide-left-leave-active,
&-slide-right-leave-active,
&-slide-bottom-leave-active {
- transition-timing-function: ease-in;
+ transition-timing-function: @animation-timing-function-leave;
}
&-slide-top-enter-from,
diff --git a/src/style/demo/index.vue b/src/style/demo/index.vue
index 8f7fbea01..959d8e27a 100644
--- a/src/style/demo/index.vue
+++ b/src/style/demo/index.vue
@@ -98,7 +98,7 @@ export default {
height: 100px;
margin: -50px 0 0 -50px;
background-color: @blue;
- border-radius: 3px;
+ border-radius: 8px;
}
}
diff --git a/src/style/var.less b/src/style/var.less
index 6738d6f89..f881793f4 100644
--- a/src/style/var.less
+++ b/src/style/var.less
@@ -550,6 +550,21 @@
@picker-loading-icon-color: @blue;
@picker-loading-mask-color: rgba(255, 255, 255, 0.9);
+// Popover
+@popover-arrow-size: 6px;
+@popover-border-radius: @border-radius-lg;
+@popover-action-width: 128px;
+@popover-action-height: 44px;
+@popover-action-font-size: @font-size-md;
+@popover-action-line-height: @line-height-md;
+@popover-action-icon-size: 20px;
+@popover-light-text-color: @text-color;
+@popover-light-background-color: @white;
+@popover-light-action-disabled-text-color: @gray-5;
+@popover-dark-text-color: @white;
+@popover-dark-background-color: #4a4a4a;
+@popover-dark-action-disabled-text-color: @gray-6;
+
// Popup
@popup-background-color: @white;
@popup-transition: transform @animation-duration-base;
diff --git a/types/index.d.ts b/types/index.d.ts
index 35631a6c5..4a41ea3ea 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -66,6 +66,7 @@ export class Overlay extends VanComponent {}
export class Pagination extends VanComponent {}
export class Panel extends VanComponent {}
export class PasswordInput extends VanComponent {}
+export class Popover extends VanComponent {}
export class Popup extends VanComponent {}
export class PullRefresh extends VanComponent {}
export class Radio extends VanComponent {}
diff --git a/vant.config.js b/vant.config.js
index 0df4f0e4e..a8ff72d9b 100644
--- a/vant.config.js
+++ b/vant.config.js
@@ -286,6 +286,10 @@ module.exports = {
path: 'notice-bar',
title: 'NoticeBar 通知栏',
},
+ {
+ path: 'popover',
+ title: 'Popover 气泡弹出框',
+ },
{
path: 'progress',
title: 'Progress 进度条',
@@ -632,6 +636,10 @@ module.exports = {
path: 'notice-bar',
title: 'NoticeBar',
},
+ {
+ path: 'popover',
+ title: 'Popover',
+ },
{
path: 'progress',
title: 'Progress',
diff --git a/yarn.lock b/yarn.lock
index 8643d67e1..fa9f9153e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1319,6 +1319,11 @@
dependencies:
"@types/node" ">= 8"
+"@popperjs/core@^2.5.4":
+ version "2.5.4"
+ resolved "https://registry.npm.taobao.org/@popperjs/core/download/@popperjs/core-2.5.4.tgz#de25b5da9f727985a3757fd59b5d028aba75841a"
+ integrity sha1-3iW12p9yeYWjdX/Vm10Cirp1hBo=
+
"@sindresorhus/is@^0.14.0":
version "0.14.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"