diff --git a/packages/components/popover/README.en-US.md b/packages/components/popover/README.en-US.md
new file mode 100644
index 000000000..9aa810f6f
--- /dev/null
+++ b/packages/components/popover/README.en-US.md
@@ -0,0 +1,37 @@
+:: BASE_DOC ::
+
+## API
+
+
+### Popover Props
+
+name | type | default | description | required
+-- | -- | -- | -- | --
+style | Object | - | CSS(Cascading Style Sheets) | N
+custom-style | Object | - | CSS(Cascading Style Sheets),used to set style on virtual component | N
+close-on-click-outside | Boolean | true | \- | N
+content | String | - | \- | N
+placement | String | top | options: top/left/right/bottom/top-left/top-right/bottom-left/bottom-right/left-top/left-bottom/right-top/right-bottom | N
+show-arrow | Boolean | true | \- | N
+theme | String | dark | options: dark/light/brand/success/warning/error | N
+visible | Boolean | - | \- | N
+
+### Popover Events
+
+name | params | description
+-- | -- | --
+visible-change | `(visible: boolean)` | \-
+
+### Popover Slots
+
+name | Description
+-- | --
+\- | \-
+content | \-
+
+### Popover External Classes
+
+className | Description
+-- | --
+t-class | \-
+t-class-content | \-
diff --git a/packages/components/popover/README.md b/packages/components/popover/README.md
new file mode 100644
index 000000000..af6c922d3
--- /dev/null
+++ b/packages/components/popover/README.md
@@ -0,0 +1,61 @@
+---
+title: Popover 弹出气泡
+description: 用于文字提示的气泡框。
+spline: data
+isComponent: true
+---
+
+
+## 引入
+
+全局引入,在 miniprogram 根目录下的`app.json`中配置,局部引入,在需要引入的页面或组件的`index.json`中配置。
+
+```json
+"usingComponents": {
+ "t-popover": "tdesign-miniprogram/popover/popover"
+}
+```
+
+
+
+
+### 组件类型
+带箭头的弹出气泡
+
+{{ base }}
+
+## API
+
+
+### Popover Props
+
+名称 | 类型 | 默认值 | 描述 | 必传
+-- | -- | -- | -- | --
+style | Object | - | 样式 | N
+custom-style | Object | - | 样式,一般用于开启虚拟化组件节点场景 | N
+close-on-click-outside | Boolean | true | 是否在点击外部元素后关闭菜单 | N
+content | String | - | 确认框内容 | N
+placement | String | top | 浮层出现位置。可选项:top/left/right/bottom/top-left/top-right/bottom-left/bottom-right/left-top/left-bottom/right-top/right-bottom | N
+show-arrow | Boolean | true | 是否显示浮层箭头 | N
+theme | String | dark | 弹出气泡主题。可选项:dark/light/brand/success/warning/error | N
+visible | Boolean | - | 是否显示气泡确认框 | N
+
+### Popover Events
+
+名称 | 参数 | 描述
+-- | -- | --
+visible-change | `(visible: boolean)` | 确认框显示或隐藏时触发
+
+### Popover Slots
+
+名称 | 描述
+-- | --
+\- | 自定义 `` 显示内容
+content \| 自定义 `content` 显示内容
+
+### Popover External Classes
+
+类名 | 描述
+-- | --
+t-class | 根节点样式类
+t-class-content | 内容样式类
diff --git a/packages/components/popover/__test__/__snapshots__/demo.test.js.snap b/packages/components/popover/__test__/__snapshots__/demo.test.js.snap
new file mode 100644
index 000000000..951476e00
--- /dev/null
+++ b/packages/components/popover/__test__/__snapshots__/demo.test.js.snap
@@ -0,0 +1,742 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Popover Popover base demo works fine 1`] = `
+
+
+
+
+
+ 弹出气泡内容
+
+
+
+
+ 带箭头
+
+
+
+
+
+
+
+
+
+ 不带箭头
+
+
+
+
+
+
+
+
+
+ 选项1
+
+
+ 选项2
+
+
+ 选项3
+
+
+
+
+
+ 自定义内容
+
+
+
+
+
+`;
+
+exports[`Popover Popover placement demo works fine 1`] = `
+
+
+
+
+
+
+
+ 弹出气泡内容
+
+
+
+
+ 顶部左
+
+
+
+
+
+
+
+
+ 弹出气泡内容
+
+
+
+
+ 顶部中
+
+
+
+
+
+
+
+
+ 弹出气泡内容
+
+
+
+
+ 顶部右
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 弹出气泡内容
+
+
+
+
+ 底部左
+
+
+
+
+
+
+
+
+ 弹出气泡内容
+
+
+
+
+ 底部中
+
+
+
+
+
+
+
+
+ 弹出气泡内容
+
+
+
+
+ 底部右
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 气泡内容
+
+
+
+
+ 右侧上
+
+
+
+
+
+
+
+
+ 气泡内容
+
+
+
+
+ 右侧中
+
+
+
+
+
+
+
+
+ 气泡内容
+
+
+
+
+ 右侧下
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 气泡内容
+
+
+
+
+ 左侧上
+
+
+
+
+
+
+
+
+ 气泡内容
+
+
+
+
+ 左侧中
+
+
+
+
+
+
+
+
+ 气泡内容
+
+
+
+
+ 左侧下
+
+
+
+
+
+
+
+
+`;
+
+exports[`Popover Popover theme demo works fine 1`] = `
+
+
+
+
+
+
+ 深色
+
+
+
+
+
+
+
+
+ 浅色
+
+
+
+
+
+
+
+
+ 品牌色
+
+
+
+
+
+
+
+
+
+
+ 成功色
+
+
+
+
+
+
+
+
+ 警告色
+
+
+
+
+
+
+
+
+ 错误色
+
+
+
+
+
+
+`;
diff --git a/packages/components/popover/__test__/demo.test.js b/packages/components/popover/__test__/demo.test.js
new file mode 100644
index 000000000..4764d110f
--- /dev/null
+++ b/packages/components/popover/__test__/demo.test.js
@@ -0,0 +1,19 @@
+/**
+ * 该文件为由脚本 `npm run test:demo` 自动生成,如需修改,执行脚本命令即可。请勿手写直接修改,否则会被覆盖
+ */
+
+import path from 'path';
+import simulate from 'miniprogram-simulate';
+
+const mapper = ['base', 'theme', 'placement'];
+
+describe('Popover', () => {
+ mapper.forEach((demoName) => {
+ it(`Popover ${demoName} demo works fine`, () => {
+ const id = load(path.resolve(__dirname, `../_example/${demoName}/index`), demoName);
+ const container = simulate.render(id);
+ container.attach(document.createElement('parent-wrapper'));
+ expect(container.toJSON()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/packages/components/popover/_example/base/index.js b/packages/components/popover/_example/base/index.js
new file mode 100644
index 000000000..56c23e43d
--- /dev/null
+++ b/packages/components/popover/_example/base/index.js
@@ -0,0 +1,22 @@
+Component({
+ data: {
+ visible: {
+ normal: false,
+ noArrow: false,
+ custom: false,
+ },
+ },
+ methods: {
+ showPopover(e) {
+ const { target } = e.currentTarget.dataset;
+ this.setData({
+ [`visible.${target}`]: !this.data.visible[target],
+ });
+ },
+ onVisibleChange(e) {
+ this.setData({
+ visible: e.detail.visible,
+ });
+ },
+ },
+});
diff --git a/packages/components/popover/_example/base/index.json b/packages/components/popover/_example/base/index.json
new file mode 100644
index 000000000..0cd2dc401
--- /dev/null
+++ b/packages/components/popover/_example/base/index.json
@@ -0,0 +1,7 @@
+{
+ "component": true,
+ "usingComponents": {
+ "t-popover": "tdesign-miniprogram/popover/popover",
+ "t-button": "tdesign-miniprogram/button/button"
+ }
+}
diff --git a/packages/components/popover/_example/base/index.wxml b/packages/components/popover/_example/base/index.wxml
new file mode 100644
index 000000000..57a9e7db8
--- /dev/null
+++ b/packages/components/popover/_example/base/index.wxml
@@ -0,0 +1,57 @@
+
+
+
+ 弹出气泡内容
+
+
+ 带箭头
+
+
+
+
+
+
+
+
+ 不带箭头
+
+
+
+
+
+
+
+
+ 选项{{ index + 1 }}
+
+
+
+
+
+ 自定义内容
+
+
+
+
diff --git a/packages/components/popover/_example/base/index.wxss b/packages/components/popover/_example/base/index.wxss
new file mode 100644
index 000000000..1bd839b1a
--- /dev/null
+++ b/packages/components/popover/_example/base/index.wxss
@@ -0,0 +1,40 @@
+.row {
+ display: flex;
+ flex-direction: column;
+}
+
+.demo-block__header-desc {
+ margin-top: var(--td-spacer, 16rpx);
+ margin-bottom: 32rpx;
+ font-size: var(--td-font-size-base, 28rpx);
+ white-space: pre-line;
+ color: var(--bg-color-demo-desc);
+ line-height: 22px;
+}
+
+.popover-example__content {
+ display: flex;
+ justify-content: center;
+}
+
+.custom {
+ --td-popover-padding: 0;
+}
+
+.custom__list {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ color: #fff;
+}
+
+.custom__item {
+ width: 105px;
+ line-height: 24px;
+ text-align: center;
+ padding: 12px;
+}
+
+.custom__item:not(:last-child) {
+ border-bottom: 1px solid #fff;
+}
diff --git a/packages/components/popover/_example/placement/index.js b/packages/components/popover/_example/placement/index.js
new file mode 100644
index 000000000..448fe7870
--- /dev/null
+++ b/packages/components/popover/_example/placement/index.js
@@ -0,0 +1,31 @@
+Component({
+ data: {
+ visible: {
+ topLeft: false,
+ top: false,
+ topRight: false,
+ bottomLeft: false,
+ bottom: false,
+ bottomRight: false,
+ leftTop: false,
+ left: false,
+ leftBottom: false,
+ rightTop: false,
+ right: false,
+ rightBottom: false,
+ },
+ },
+ methods: {
+ showPopover(e) {
+ const { target } = e.currentTarget.dataset;
+ this.setData({
+ [`visible.${target}`]: !this.data.visible[target],
+ });
+ },
+ onVisibleChange(e) {
+ this.setData({
+ visible: e.detail.visible,
+ });
+ },
+ },
+});
diff --git a/packages/components/popover/_example/placement/index.json b/packages/components/popover/_example/placement/index.json
new file mode 100644
index 000000000..0cd2dc401
--- /dev/null
+++ b/packages/components/popover/_example/placement/index.json
@@ -0,0 +1,7 @@
+{
+ "component": true,
+ "usingComponents": {
+ "t-popover": "tdesign-miniprogram/popover/popover",
+ "t-button": "tdesign-miniprogram/button/button"
+ }
+}
diff --git a/packages/components/popover/_example/placement/index.wxml b/packages/components/popover/_example/placement/index.wxml
new file mode 100644
index 000000000..0d6ca8ed1
--- /dev/null
+++ b/packages/components/popover/_example/placement/index.wxml
@@ -0,0 +1,299 @@
+
+
+
+
+
+ 弹出气泡内容
+
+
+ 顶部左
+
+
+
+
+
+
+ 弹出气泡内容
+
+
+ 顶部中
+
+
+
+
+
+
+ 弹出气泡内容
+
+
+ 顶部右
+
+
+
+
+
+
+
+
+
+
+
+
+ 弹出气泡内容
+
+
+ 底部左
+
+
+
+
+
+
+ 弹出气泡内容
+
+
+ 底部中
+
+
+
+
+
+
+ 弹出气泡内容
+
+
+ 底部右
+
+
+
+
+
+
+
+
+
+
+
+
+ 气泡内容
+
+
+ 右侧上
+
+
+
+
+
+
+ 气泡内容
+
+
+ 右侧中
+
+
+
+
+
+
+ 气泡内容
+
+
+ 右侧下
+
+
+
+
+
+
+
+
+
+
+
+
+ 气泡内容
+
+
+ 左侧上
+
+
+
+
+
+
+ 气泡内容
+
+
+ 左侧中
+
+
+
+
+
+
+ 气泡内容
+
+
+ 左侧下
+
+
+
+
+
+
diff --git a/packages/components/popover/_example/placement/index.wxss b/packages/components/popover/_example/placement/index.wxss
new file mode 100644
index 000000000..47d292c45
--- /dev/null
+++ b/packages/components/popover/_example/placement/index.wxss
@@ -0,0 +1,43 @@
+.popover-example-row {
+ display: flex;
+ flex-direction: column;
+ padding: 0 32rpx;
+ margin-bottom: 48rpx;
+}
+
+.row {
+ display: flex;
+ flex-direction: row;
+ gap: 32rpx;
+}
+
+.column {
+ display: flex;
+ flex-direction: column;
+ gap: 32rpx;
+}
+
+.flex-end .column {
+ align-items: flex-end;
+}
+
+.demo-block__header-desc {
+ margin-top: var(--td-spacer, 16rpx);
+ margin-bottom: 32rpx;
+ font-size: var(--td-font-size-base, 28rpx);
+ white-space: pre-line;
+ color: var(--bg-color-demo-desc);
+ line-height: 22px;
+}
+
+.popover-example__content {
+ flex: 1;
+}
+
+.button-width--small {
+ width: 204rpx;
+}
+
+.button-with--large {
+ width: 446rpx;
+}
diff --git a/packages/components/popover/_example/popover.json b/packages/components/popover/_example/popover.json
new file mode 100644
index 000000000..2d42c5267
--- /dev/null
+++ b/packages/components/popover/_example/popover.json
@@ -0,0 +1,9 @@
+{
+ "navigationBarTitleText": "Popover",
+ "navigationBarBackgroundColor": "#fff",
+ "usingComponents": {
+ "base": "./base",
+ "theme": "./theme",
+ "placement": "./placement"
+ }
+}
diff --git a/packages/components/popover/_example/popover.less b/packages/components/popover/_example/popover.less
new file mode 100644
index 000000000..1837d8a94
--- /dev/null
+++ b/packages/components/popover/_example/popover.less
@@ -0,0 +1,4 @@
+page {
+ background-color: var(--td-bg-color-container);
+ padding-bottom: 48rpx;
+}
diff --git a/packages/components/popover/_example/popover.ts b/packages/components/popover/_example/popover.ts
new file mode 100644
index 000000000..560d44d43
--- /dev/null
+++ b/packages/components/popover/_example/popover.ts
@@ -0,0 +1 @@
+Page({});
diff --git a/packages/components/popover/_example/popover.wxml b/packages/components/popover/_example/popover.wxml
new file mode 100644
index 000000000..477f6a013
--- /dev/null
+++ b/packages/components/popover/_example/popover.wxml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/components/popover/_example/theme/index.js b/packages/components/popover/_example/theme/index.js
new file mode 100644
index 000000000..56b090650
--- /dev/null
+++ b/packages/components/popover/_example/theme/index.js
@@ -0,0 +1,25 @@
+Component({
+ data: {
+ visible: {
+ dark: false,
+ light: false,
+ success: false,
+ brand: false,
+ warning: false,
+ error: false,
+ },
+ },
+ methods: {
+ showPopover(e) {
+ const { target } = e.currentTarget.dataset;
+ this.setData({
+ [`visible.${target}`]: !this.data.visible[target],
+ });
+ },
+ onVisibleChange(e) {
+ this.setData({
+ visible: e.detail.visible,
+ });
+ },
+ },
+});
diff --git a/packages/components/popover/_example/theme/index.json b/packages/components/popover/_example/theme/index.json
new file mode 100644
index 000000000..0cd2dc401
--- /dev/null
+++ b/packages/components/popover/_example/theme/index.json
@@ -0,0 +1,7 @@
+{
+ "component": true,
+ "usingComponents": {
+ "t-popover": "tdesign-miniprogram/popover/popover",
+ "t-button": "tdesign-miniprogram/button/button"
+ }
+}
diff --git a/packages/components/popover/_example/theme/index.wxml b/packages/components/popover/_example/theme/index.wxml
new file mode 100644
index 000000000..92e5fbb11
--- /dev/null
+++ b/packages/components/popover/_example/theme/index.wxml
@@ -0,0 +1,130 @@
+
+
+
+
+ 深色
+
+
+
+
+
+
+ 浅色
+
+
+
+
+
+
+ 品牌色
+
+
+
+
+
+
+
+
+ 成功色
+
+
+
+
+
+
+ 警告色
+
+
+
+
+
+
+ 错误色
+
+
+
+
diff --git a/packages/components/popover/_example/theme/index.wxss b/packages/components/popover/_example/theme/index.wxss
new file mode 100644
index 000000000..14ea5249d
--- /dev/null
+++ b/packages/components/popover/_example/theme/index.wxss
@@ -0,0 +1,22 @@
+.row {
+ display: flex;
+ padding: 0 32rpx;
+ gap: 32rpx;
+}
+
+.demo-block__header-desc {
+ margin-top: var(--td-spacer, 16rpx);
+ margin-bottom: 32rpx;
+ font-size: var(--td-font-size-base, 28rpx);
+ white-space: pre-line;
+ color: var(--bg-color-demo-desc);
+ line-height: 22px;
+}
+
+.popover-example__content {
+ flex: 1;
+}
+
+.button-width--small {
+ width: 204rpx;
+}
diff --git a/packages/components/popover/index.ts b/packages/components/popover/index.ts
new file mode 100644
index 000000000..07c78605c
--- /dev/null
+++ b/packages/components/popover/index.ts
@@ -0,0 +1,3 @@
+export * from './props';
+export * from './type';
+export * from './popover';
diff --git a/packages/components/popover/popover.json b/packages/components/popover/popover.json
new file mode 100644
index 000000000..3b3501e30
--- /dev/null
+++ b/packages/components/popover/popover.json
@@ -0,0 +1,7 @@
+{
+ "component": true,
+ "styleIsolation": "apply-shared",
+ "usingComponents": {
+ "t-overlay": "../overlay/overlay"
+ }
+}
diff --git a/packages/components/popover/popover.less b/packages/components/popover/popover.less
new file mode 100644
index 000000000..cc0491bed
--- /dev/null
+++ b/packages/components/popover/popover.less
@@ -0,0 +1,278 @@
+@import '../common/style/base.less';
+
+@popover: ~'@{prefix}-popover';
+
+// 主题色变量
+@popover-padding: var(--td-popover-padding, 24rpx);
+@popover-arrow-width: 16rpx;
+@popover-content-margin: 16rpx;
+
+// 主题色变量
+@popover-dark-color: #fff;
+@popover-dark-bg-color: @font-gray-1;
+@popover-light-color: @text-color-primary;
+@popover-light-bg-color: @bg-color-container;
+@popover-brand-color: @primary-color-7;
+@popover-brand-bg-color: @primary-color-1;
+@popover-success-color: @success-color-5;
+@popover-success-bg-color: @success-color-1;
+@popover-warning-color: @warning-color-5;
+@popover-warning-bg-color: @warning-color-1;
+@popover-error-color: @error-color-6;
+@popover-error-bg-color: @error-color-1;
+
+.@{popover}__wrapper {
+ display: inline-block;
+}
+
+.@{popover} {
+ position: absolute;
+ z-index: 11500;
+ overflow: visible;
+ transition: 0.2s ease-in-out all;
+
+ &__content {
+ position: relative;
+ padding: @popover-padding;
+ border-radius: 12rpx;
+ box-shadow: @shadow-3;
+ font-size: @font-size-m;
+ line-height: 48rpx;
+ box-sizing: border-box;
+ word-break: break-all;
+
+ border-radius: 6px;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ word-break: break-all;
+ }
+
+ &__arrow {
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-style: solid;
+ border-color: transparent;
+ border-width: @popover-arrow-width;
+ }
+
+ // 主题
+ .popover-theme(dark);
+ .popover-theme(light);
+ .popover-theme(brand);
+ .popover-theme(success);
+ .popover-theme(warning);
+ .popover-theme(error);
+
+ &.@{prefix}-fade-enter-to {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ &.@{prefix}-fade-enter,
+ &.@{prefix}-fade-leave-to {
+ opacity: 0;
+ visibility: hidden;
+ }
+}
+
+// 箭头方向与偏移
+.content-placement-top();
+.content-placement-bottom();
+.content-placement-left();
+.content-placement-right();
+
+.arrow-placement-top();
+.arrow-placement-bottom();
+.arrow-placement-left();
+.arrow-placement-right();
+
+.content-placement-top {
+ .@{prefix}-popover[data-placement^='top'] {
+ .@{prefix}-popover__content {
+ margin-bottom: @popover-content-margin;
+ }
+ }
+}
+
+.content-placement-bottom {
+ .@{prefix}-popover[data-placement^='bottom'] {
+ .@{prefix}-popover__content {
+ margin-top: @popover-content-margin;
+ }
+ }
+}
+
+.content-placement-left {
+ .@{prefix}-popover[data-placement^='left'] {
+ .@{prefix}-popover__content {
+ margin-right: @popover-content-margin;
+ }
+ }
+}
+
+.content-placement-right {
+ .@{prefix}-popover[data-placement^='right'] {
+ .@{prefix}-popover__content {
+ margin-left: @popover-content-margin;
+ }
+ }
+}
+
+.arrow-placement-top() {
+ .@{prefix}-popover[data-placement^='top'] {
+ .@{prefix}-popover__arrow {
+ bottom: 0;
+ border-top-color: currentColor;
+ border-bottom-width: 0;
+ margin-bottom: calc(@popover-arrow-width * -1);
+ }
+ }
+
+ .@{prefix}-popover[data-placement='top'] {
+ transform-origin: 50% 100%;
+
+ .@{prefix}-popover__arrow {
+ left: 50%;
+ transform: translateX(-50%);
+ }
+ }
+
+ .@{prefix}-popover[data-placement='top-start'] {
+ transform-origin: 0 100%;
+
+ .@{prefix}-popover__arrow {
+ left: @popover-padding;
+ }
+ }
+
+ .@{prefix}-popover[data-placement='top-end'] {
+ transform-origin: 100% 100%;
+
+ .@{prefix}-popover__arrow {
+ right: @popover-padding;
+ }
+ }
+}
+
+.arrow-placement-left() {
+ .@{prefix}-popover[data-placement^='left'] {
+ .@{prefix}-popover__arrow {
+ right: 0;
+ border-right-width: 0;
+ border-left-color: currentColor;
+ margin-right: calc(@popover-arrow-width * -1);
+ }
+ }
+
+ .@{prefix}-popover[data-placement='left'] {
+ transform-origin: 100% 50%;
+
+ .@{prefix}-popover__arrow {
+ top: 50%;
+ transform: translateY(-50%);
+ }
+ }
+
+ .@{prefix}-popover[data-placement='left-start'] {
+ transform-origin: 100% 0;
+
+ .@{prefix}-popover__arrow {
+ top: @popover-padding;
+ }
+ }
+
+ .@{prefix}-popover[data-placement='left-end'] {
+ transform-origin: 100% 100%;
+
+ .@{prefix}-popover__arrow {
+ bottom: @popover-padding;
+ }
+ }
+}
+
+.arrow-placement-bottom() {
+ .@{prefix}-popover[data-placement^='bottom'] {
+ .@{prefix}-popover__arrow {
+ top: 0;
+ border-top-width: 0;
+ border-bottom-color: currentColor;
+ margin-top: calc(@popover-arrow-width * -1);
+ }
+ }
+
+ .@{prefix}-popover[data-placement='bottom'] {
+ transform-origin: 50% 0;
+
+ .@{prefix}-popover__arrow {
+ left: 50%;
+ transform: translateX(-50%);
+ }
+ }
+
+ .@{prefix}-popover[data-placement='bottom-start'] {
+ transform-origin: 0 0;
+
+ .@{prefix}-popover__arrow {
+ left: @popover-padding;
+ }
+ }
+
+ .@{prefix}-popover[data-placement='bottom-end'] {
+ transform-origin: 100% 0;
+
+ .@{prefix}-popover__arrow {
+ right: @popover-padding;
+ }
+ }
+}
+
+.arrow-placement-right() {
+ .@{prefix}-popover[data-placement^='right'] {
+ .@{prefix}-popover__arrow {
+ left: 0;
+ border-right-color: currentColor;
+ border-left-width: 0;
+ margin-left: calc(@popover-arrow-width * -1);
+ }
+ }
+
+ .@{prefix}-popover[data-placement='right'] {
+ transform-origin: 0 50%;
+
+ .@{prefix}-popover__arrow {
+ top: 50%;
+ transform: translateY(-50%);
+ }
+ }
+
+ .@{prefix}-popover[data-placement='right-start'] {
+ transform-origin: 0 0;
+
+ .@{prefix}-popover__arrow {
+ top: @popover-padding;
+ }
+ }
+
+ .@{prefix}-popover[data-placement='right-end'] {
+ transform-origin: 0 100%;
+
+ .@{prefix}-popover__arrow {
+ bottom: @popover-padding;
+ }
+ }
+}
+
+.popover-theme(@theme) {
+ @color: 'popover-@{theme}-color';
+ @bgColor: 'popover-@{theme}-bg-color';
+
+ .@{prefix}-popover--@{theme} {
+ color: @@color;
+ background: @@bgColor;
+
+ .@{prefix}-popover__arrow {
+ color: @@bgColor;
+ }
+ }
+}
diff --git a/packages/components/popover/popover.ts b/packages/components/popover/popover.ts
new file mode 100644
index 000000000..213b58275
--- /dev/null
+++ b/packages/components/popover/popover.ts
@@ -0,0 +1,175 @@
+import { getWindowInfo } from '../common/wechat';
+import { TdPopoverProps } from './type';
+import { SuperComponent, wxComponent } from '../common/src/index';
+import config from '../common/config';
+import props from './props';
+import { unitConvert } from '../common/utils';
+import transition from '../mixins/transition';
+
+delete props.visible;
+
+export interface PopoverProps extends TdPopoverProps {}
+
+const { prefix } = config;
+const name = `${prefix}-popover`;
+
+@wxComponent()
+export default class Popover extends SuperComponent {
+ behaviors = [transition()];
+
+ externalClasses = [`${prefix}-class`, `${prefix}-class-content`, `${prefix}-class-trigger`];
+
+ options = {
+ multipleSlots: true,
+ };
+
+ properties = props;
+
+ data = {
+ prefix,
+ classPrefix: name,
+ _placement: 'top',
+ contentStyle: '',
+ arrowStyle: '',
+ };
+
+ observers = {
+ visible(val: boolean) {
+ if (val === undefined || val === null) return;
+ this.updateVisible(val);
+ },
+ placement() {
+ if (this.data.realVisible) this.computePosition();
+ },
+ realVisible(v: boolean) {
+ if (v) {
+ this.computePosition();
+ }
+ },
+ };
+
+ lifetimes = {
+ attached() {
+ if (this.properties.defaultVisible) {
+ this.updateVisible(true);
+ }
+ },
+ };
+
+ methods = {
+ updateVisible(visible: boolean) {
+ if (visible === this.data.visible) return;
+ this.setData({ visible }, () => {
+ this.triggerEvent('visible-change', { visible });
+ });
+ },
+
+ onOverlayTap() {
+ if (this.properties.closeOnClickOutside) {
+ this.updateVisible(false);
+ }
+ },
+
+ calcArrowStyle(placement: string, contentDom: any, popoverDom: any) {
+ const horizontal = ['top', 'bottom'];
+ const vertical = ['left', 'right'];
+ const isBase = [...horizontal, ...vertical].find((item) => item === placement);
+ if (isBase) {
+ return '';
+ }
+
+ const { width, left } = contentDom;
+ const { width: popperWidth, height: popperHeight } = popoverDom;
+ const { windowWidth } = getWindowInfo();
+
+ const isHorizontal = horizontal.find((item) => placement.includes(item));
+ const isVertical = vertical.find((item) => placement.includes(item));
+ const isEnd = placement.includes('end');
+
+ if (isHorizontal) {
+ const padding = isEnd ? Math.min(width + left, popperWidth) : Math.min(windowWidth - left, popperWidth);
+ if (isEnd) {
+ return `left:${padding - 22}px;`;
+ }
+ return `right:${padding - 22}px;`;
+ }
+ if (isVertical) {
+ const offset = popperHeight - 22;
+ if (isEnd) {
+ return `top:${offset}px;`;
+ }
+ return `bottom:${offset}px;top:unset;`;
+ }
+ return '';
+ },
+
+ async computePosition() {
+ const { placement } = this.data;
+ const _placement = placement.replace(/-(left|top)$/, '-start').replace(/-(right|bottom)$/, '-end');
+ this.setData({ _placement });
+ const query = this.createSelectorQuery();
+ query.select(`#${name}-wrapper`).boundingClientRect();
+ query.select(`#${name}-content`).boundingClientRect();
+
+ query.selectViewport().scrollOffset();
+ query.exec((res) => {
+ const [triggerRect, contentRect, viewportOffset] = res;
+ if (!triggerRect || !contentRect) return;
+
+ const offset = unitConvert(8);
+ let top = 0;
+ let left = 0;
+
+ const isTopBase = _placement.startsWith('top');
+ const isBottomBase = _placement.startsWith('bottom');
+ const isLeftBase = _placement.startsWith('left');
+ const isRightBase = _placement.startsWith('right');
+
+ if (isTopBase) {
+ top = triggerRect.top - contentRect.height - offset;
+ } else if (isBottomBase) {
+ top = triggerRect.top + triggerRect.height + offset;
+ } else if (isLeftBase) {
+ left = triggerRect.left - contentRect.width - offset;
+ } else if (isRightBase) {
+ left = triggerRect.left + triggerRect.width + offset;
+ } else {
+ top = triggerRect.top - contentRect.height - offset;
+ }
+
+ const isStart = _placement.includes('start');
+ const isEnd = _placement.includes('end');
+
+ // 垂直方向的水平居中/偏移
+ if (isTopBase || isBottomBase) {
+ if (isStart) {
+ left = triggerRect.left;
+ } else if (isEnd) {
+ left = triggerRect.left + triggerRect.width - contentRect.width;
+ } else {
+ left = triggerRect.left + triggerRect.width / 2 - contentRect.width / 2;
+ }
+ }
+
+ // 水平方向的垂直居中/偏移
+ if (isLeftBase || isRightBase) {
+ if (isStart) {
+ top = triggerRect.top;
+ } else if (isEnd) {
+ top = triggerRect.top + triggerRect.height - contentRect.height;
+ } else {
+ top = triggerRect.top + triggerRect.height / 2 - contentRect.height / 2;
+ }
+ }
+
+ const { scrollTop = 0, scrollLeft = 0 } = viewportOffset;
+ top += scrollTop;
+ left += scrollLeft;
+
+ const style = `top:${Math.max(top, 0)}px;left:${Math.max(left, 0)}px;`;
+ const arrowStyle = this.calcArrowStyle(_placement, triggerRect, contentRect);
+ this.setData({ contentStyle: style, arrowStyle });
+ });
+ },
+ };
+}
diff --git a/packages/components/popover/popover.wxml b/packages/components/popover/popover.wxml
new file mode 100644
index 000000000..f43885327
--- /dev/null
+++ b/packages/components/popover/popover.wxml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+ {{content}}
+
+
+
+
diff --git a/packages/components/popover/props.ts b/packages/components/popover/props.ts
new file mode 100644
index 000000000..f99c3e667
--- /dev/null
+++ b/packages/components/popover/props.ts
@@ -0,0 +1,44 @@
+/* eslint-disable */
+
+/**
+ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
+ * */
+
+import { TdPopoverProps } from './type';
+const props: TdPopoverProps = {
+ /** 是否在点击外部元素后关闭菜单 */
+ closeOnClickOutside: {
+ type: Boolean,
+ value: true,
+ },
+ /** 确认框内容 */
+ content: {
+ type: String,
+ },
+ /** 浮层出现位置 */
+ placement: {
+ type: String,
+ value: 'top',
+ },
+ /** 是否显示浮层箭头 */
+ showArrow: {
+ type: Boolean,
+ value: true,
+ },
+ /** 弹出气泡主题 */
+ theme: {
+ type: String,
+ value: 'dark',
+ },
+ /** 是否显示气泡确认框 */
+ visible: {
+ type: Boolean,
+ value: null,
+ },
+ /** 是否显示气泡确认框,非受控属性 */
+ defaultVisible: {
+ type: Boolean,
+ },
+};
+
+export default props;
diff --git a/packages/components/popover/type.ts b/packages/components/popover/type.ts
new file mode 100644
index 000000000..d00b905c7
--- /dev/null
+++ b/packages/components/popover/type.ts
@@ -0,0 +1,73 @@
+/* eslint-disable */
+
+/**
+ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
+ * */
+
+export interface TdPopoverProps {
+ /**
+ * 是否在点击外部元素后关闭菜单
+ * @default true
+ */
+ closeOnClickOutside?: {
+ type: BooleanConstructor;
+ value?: boolean;
+ };
+ /**
+ * 确认框内容
+ */
+ content?: {
+ type: StringConstructor;
+ value?: string;
+ };
+ /**
+ * 浮层出现位置
+ * @default top
+ */
+ placement?: {
+ type: StringConstructor;
+ value?:
+ | 'top'
+ | 'left'
+ | 'right'
+ | 'bottom'
+ | 'top-left'
+ | 'top-right'
+ | 'bottom-left'
+ | 'bottom-right'
+ | 'left-top'
+ | 'left-bottom'
+ | 'right-top'
+ | 'right-bottom';
+ };
+ /**
+ * 是否显示浮层箭头
+ * @default true
+ */
+ showArrow?: {
+ type: BooleanConstructor;
+ value?: boolean;
+ };
+ /**
+ * 弹出气泡主题
+ * @default dark
+ */
+ theme?: {
+ type: StringConstructor;
+ value?: 'dark' | 'light' | 'brand' | 'success' | 'warning' | 'error';
+ };
+ /**
+ * 是否显示气泡确认框
+ */
+ visible?: {
+ type: BooleanConstructor;
+ value?: boolean;
+ };
+ /**
+ * 是否显示气泡确认框,非受控属性
+ */
+ defaultVisible?: {
+ type: BooleanConstructor;
+ value?: boolean;
+ };
+}
diff --git a/packages/tdesign-miniprogram/example/app.json b/packages/tdesign-miniprogram/example/app.json
index c82479ef4..8641e8f91 100644
--- a/packages/tdesign-miniprogram/example/app.json
+++ b/packages/tdesign-miniprogram/example/app.json
@@ -45,6 +45,7 @@
"pages/tab-bar/tab-bar",
"pages/tab-bar/skyline/tab-bar",
"pages/transition/transition",
+ "pages/popover/popover",
"pages/popup/popup",
"pages/popup/skyline/popup",
"pages/steps/steps",
diff --git a/packages/tdesign-miniprogram/example/project.config.json b/packages/tdesign-miniprogram/example/project.config.json
index 11170e0fc..927634ee4 100644
--- a/packages/tdesign-miniprogram/example/project.config.json
+++ b/packages/tdesign-miniprogram/example/project.config.json
@@ -274,6 +274,12 @@
"query": "",
"scene": null
},
+ {
+ "name": "popover",
+ "pathName": "pages/popover/popover",
+ "query": "",
+ "scene": null
+ },
{
"name": "popup",
"pathName": "pages/popup/popup",
diff --git a/packages/tdesign-miniprogram/site/docs/overview.en-US.md b/packages/tdesign-miniprogram/site/docs/overview.en-US.md
index 07dcf1d04..9583752ba 100644
--- a/packages/tdesign-miniprogram/site/docs/overview.en-US.md
+++ b/packages/tdesign-miniprogram/site/docs/overview.en-US.md
@@ -448,6 +448,14 @@ spline: explain
+
+
+
+
diff --git a/packages/tdesign-miniprogram/site/site.config.mjs b/packages/tdesign-miniprogram/site/site.config.mjs
index 68fef328d..a8b4400f7 100644
--- a/packages/tdesign-miniprogram/site/site.config.mjs
+++ b/packages/tdesign-miniprogram/site/site.config.mjs
@@ -571,6 +571,14 @@ export const docs = [
path: '/miniprogram/components/overlay',
component: () => import('@/overlay/README.md'),
},
+ {
+ title: 'Popover 弹出气泡',
+ titleEn: 'Popover',
+ name: 'popover',
+ meta: { docType: 'message' },
+ path: '/miniprogram/components/popover',
+ component: () => import('@/popover/README.md'),
+ },
{
title: 'Popup 弹出层',
titleEn: 'Popup',