From c6b568858b60c077dec0cc161c3935e71e7986c7 Mon Sep 17 00:00:00 2001
From: Wesley <985189328@qq.com>
Date: Mon, 10 Nov 2025 18:20:40 +0800
Subject: [PATCH 01/11] feat: api
---
packages/components/popover/README.en-US.md | 37 +++++++++++
packages/components/popover/README.md | 37 +++++++++++
packages/components/popover/props.ts | 44 +++++++++++++
packages/components/popover/type.ts | 73 +++++++++++++++++++++
4 files changed, 191 insertions(+)
create mode 100644 packages/components/popover/README.en-US.md
create mode 100644 packages/components/popover/README.md
create mode 100644 packages/components/popover/props.ts
create mode 100644 packages/components/popover/type.ts
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..b5e44afe8
--- /dev/null
+++ b/packages/components/popover/README.md
@@ -0,0 +1,37 @@
+:: BASE_DOC ::
+
+## 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/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;
+ };
+}
From e54661b93a8969a8ba75f50451bb4267a3d4c7d5 Mon Sep 17 00:00:00 2001
From: Wesley <985189328@qq.com>
Date: Mon, 10 Nov 2025 23:01:16 +0800
Subject: [PATCH 02/11] chore: config
---
packages/tdesign-miniprogram/example/app.json | 1 +
packages/tdesign-miniprogram/example/project.config.json | 6 ++++++
packages/tdesign-miniprogram/site/docs/overview.en-US.md | 8 ++++++++
packages/tdesign-miniprogram/site/docs/overview.md | 8 ++++++++
packages/tdesign-miniprogram/site/site.config.mjs | 8 ++++++++
5 files changed, 31 insertions(+)
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',
From fa5cbba95a7578b6b23a3695099199fce51ad7d9 Mon Sep 17 00:00:00 2001
From: Wesley <985189328@qq.com>
Date: Tue, 11 Nov 2025 20:10:51 +0800
Subject: [PATCH 03/11] feat: popover
---
packages/components/popover/README.md | 26 +++-
packages/components/popover/index.ts | 3 +
packages/components/popover/popover.json | 8 ++
packages/components/popover/popover.less | 170 +++++++++++++++++++++++
packages/components/popover/popover.ts | 142 +++++++++++++++++++
packages/components/popover/popover.wxml | 32 +++++
packages/components/popover/popover.wxs | 18 +++
7 files changed, 398 insertions(+), 1 deletion(-)
create mode 100644 packages/components/popover/index.ts
create mode 100644 packages/components/popover/popover.json
create mode 100644 packages/components/popover/popover.less
create mode 100644 packages/components/popover/popover.ts
create mode 100644 packages/components/popover/popover.wxml
create mode 100644 packages/components/popover/popover.wxs
diff --git a/packages/components/popover/README.md b/packages/components/popover/README.md
index b5e44afe8..af6c922d3 100644
--- a/packages/components/popover/README.md
+++ b/packages/components/popover/README.md
@@ -1,4 +1,28 @@
-:: BASE_DOC ::
+---
+title: Popover 弹出气泡
+description: 用于文字提示的气泡框。
+spline: data
+isComponent: true
+---
+
+
+## 引入
+
+全局引入,在 miniprogram 根目录下的`app.json`中配置,局部引入,在需要引入的页面或组件的`index.json`中配置。
+
+```json
+"usingComponents": {
+ "t-popover": "tdesign-miniprogram/popover/popover"
+}
+```
+
+
+
+
+### 组件类型
+带箭头的弹出气泡
+
+{{ base }}
## API
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..babc8a1f1
--- /dev/null
+++ b/packages/components/popover/popover.json
@@ -0,0 +1,8 @@
+{
+ "component": true,
+ "styleIsolation": "apply-shared",
+ "usingComponents": {
+ "t-overlay": "../overlay/overlay",
+ "t-icon": "../icon/icon"
+ }
+}
diff --git a/packages/components/popover/popover.less b/packages/components/popover/popover.less
new file mode 100644
index 000000000..fb408dfcc
--- /dev/null
+++ b/packages/components/popover/popover.less
@@ -0,0 +1,170 @@
+@import '../common/style/base.less';
+
+@popover: ~'@{prefix}-popover';
+
+// 主题色变量
+@popover-padding: 24rpx;
+@popover-arrow: 12rpx;
+
+.@{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;
+ }
+
+ // 主题
+ &--dark {
+ color: #fff;
+ background: @font-gray-1;
+
+ .@{popover}__arrow {
+ color: @font-gray-1;
+ }
+ }
+
+ &--light {
+ color: @text-color-primary;
+ background: @bg-color-container;
+
+ .@{popover}__arrow {
+ color: @bg-color-container;
+ }
+ }
+
+ &--brand {
+ color: @primary-color-7;
+ background: @primary-color-1;
+
+ .@{popover}__arrow {
+ color: @primary-color-1;
+ }
+ }
+
+ &--success {
+ color: @success-color-5;
+ background: @success-color-1;
+
+ .@{popover}__arrow {
+ color: @success-color-1;
+ }
+ }
+
+ &--warning {
+ color: @warning-color-5;
+ background: @warning-color-1;
+
+ .@{popover}__arrow {
+ color: @warning-color-1;
+ }
+ }
+
+ &--error {
+ color: @error-color-6;
+ background: @error-color-1;
+
+ .@{popover}__arrow {
+ color: @error-color-1;
+ }
+ }
+
+ // 箭头方向与偏移
+ &[data-placement^='top'] {
+ transform-origin: 50% 100%;
+ .@{popover}__content {
+ margin-bottom: 16rpx;
+ }
+ .@{popover}__arrow {
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ border-bottom-width: 0;
+ border-top-color: currentColor;
+ margin-bottom: -@popover-arrow;
+ }
+ }
+
+ &[data-placement^='bottom'] {
+ transform-origin: 50% 0;
+ .@{popover}__content {
+ margin-top: 16rpx;
+ }
+ .@{popover}__arrow {
+ top: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ border-top-width: 0;
+ border-bottom-color: currentColor;
+ margin-top: -@popover-arrow;
+ }
+ }
+
+ &[data-placement^='left'] {
+ transform-origin: 100% 50%;
+ .@{popover}__content {
+ margin-right: 16rpx;
+ }
+ .@{popover}__arrow {
+ right: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ border-right-width: 0;
+ border-left-color: currentColor;
+ margin-right: -@popover-arrow;
+ }
+ }
+
+ &[data-placement^='right'] {
+ transform-origin: 0 50%;
+ .@{popover}__content {
+ margin-left: 16rpx;
+ }
+ .@{popover}__arrow {
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ border-left-width: 0;
+ border-right-color: currentColor;
+ margin-left: -@popover-arrow;
+ }
+ }
+
+ &.@{prefix}-fade-enter-to {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ &.@{prefix}-fade-enter,
+ &.@{prefix}-fade-leave-to {
+ opacity: 0;
+ visibility: hidden;
+ }
+}
diff --git a/packages/components/popover/popover.ts b/packages/components/popover/popover.ts
new file mode 100644
index 000000000..34092b76b
--- /dev/null
+++ b/packages/components/popover/popover.ts
@@ -0,0 +1,142 @@
+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';
+
+// 保留 visible 以支持受控用法
+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',
+ theme: 'dark',
+ contentStyle: '',
+ _triggerRect: null as WechatMiniprogram.BoundingClientRectCallbackResult | null,
+ };
+
+ observers = {
+ visible(v: boolean) {
+ if (v === undefined || v === null) return;
+ this.updateVisible(v, 'prop');
+ },
+ placement() {
+ if (this.data.realVisible) this.computePosition();
+ },
+ realVisible(v: boolean) {
+ if (v) {
+ this.computePosition();
+ }
+ },
+ };
+
+ lifetimes = {
+ attached() {
+ if (this.properties.defaultVisible) {
+ this.updateVisible(true, 'default');
+ }
+ },
+ };
+
+ methods = {
+ updateVisible(visible: boolean, trigger: string) {
+ if (visible === this.data.visible) return;
+ this.setData({ visible }, () => {
+ this.triggerEvent('visible-change', { visible, trigger });
+ });
+ },
+
+ onToggle() {
+ const { realVisible } = this.data;
+ this.updateVisible(!realVisible, 'trigger');
+ },
+
+ onOverlayTap() {
+ if (this.properties.closeOnClickOutside) {
+ this.updateVisible(false, 'overlay');
+ }
+ },
+
+ async computePosition() {
+ // 计算触发元素和内容尺寸,设置 contentStyle
+ // 简化:仅处理四个基础方向 top/right/bottom/left 以及带 start/end 的 12 种。
+ const { placement } = this.data;
+ const query = this.createSelectorQuery();
+ query.select(`#${name}-wrapper`).boundingClientRect();
+ query.select(`#${name}-content`).boundingClientRect();
+ query.exec((res) => {
+ const [triggerRect, contentRect] = res as [
+ WechatMiniprogram.BoundingClientRectCallbackResult,
+ WechatMiniprogram.BoundingClientRectCallbackResult,
+ ];
+ if (!triggerRect || !contentRect) return;
+ const offset = unitConvert(8); // 间距 8rpx => px
+ let top = 0;
+ let left = 0;
+
+ const base = placement.split('-')[0];
+ const second = placement.split('-')[1];
+
+ switch (base) {
+ case 'top':
+ top = triggerRect.top - contentRect.height - offset;
+ break;
+ case 'bottom':
+ top = triggerRect.top + triggerRect.height + offset;
+ break;
+ case 'left':
+ left = triggerRect.left - contentRect.width - offset;
+ break;
+ case 'right':
+ left = triggerRect.left + triggerRect.width + offset;
+ break;
+ default:
+ top = triggerRect.top - contentRect.height - offset;
+ }
+
+ // 垂直方向的水平居中/偏移
+ if (['top', 'bottom'].includes(base)) {
+ if (!second) {
+ left = triggerRect.left + triggerRect.width / 2 - contentRect.width / 2;
+ } else if (second === 'left') {
+ left = triggerRect.left;
+ } else if (second === 'right') {
+ left = triggerRect.left + triggerRect.width - contentRect.width;
+ }
+ }
+ // 水平方向的垂直居中/偏移
+ if (['left', 'right'].includes(base)) {
+ if (!second) {
+ top = triggerRect.top + triggerRect.height / 2 - contentRect.height / 2;
+ } else if (second === 'top') {
+ top = triggerRect.top;
+ } else if (second === 'bottom') {
+ top = triggerRect.top + triggerRect.height - contentRect.height;
+ }
+ }
+
+ const style = `top:${Math.max(top, 0)}px;left:${Math.max(left, 0)}px;`;
+ this.setData({ contentStyle: style, _triggerRect: triggerRect });
+ });
+ },
+ };
+}
diff --git a/packages/components/popover/popover.wxml b/packages/components/popover/popover.wxml
new file mode 100644
index 000000000..d1a4f916e
--- /dev/null
+++ b/packages/components/popover/popover.wxml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/components/popover/popover.wxs b/packages/components/popover/popover.wxs
new file mode 100644
index 000000000..2f4a01f01
--- /dev/null
+++ b/packages/components/popover/popover.wxs
@@ -0,0 +1,18 @@
+function getPopupStyles(zIndex, distanceTop, placement) {
+ var zIndexStyle = zIndex ? 'z-index:' + zIndex + ';' : '';
+ if ((placement === 'top' || placement === 'left' || placement === 'right') && distanceTop) {
+ zIndexStyle = zIndexStyle + 'top:' + distanceTop + 'px;' + '--td-popup-distance-top:' + distanceTop + 'px;';
+ }
+ return zIndexStyle;
+}
+
+function onContentTouchMove(e) {
+ if (e.target && e.target.dataset.prevention) {
+ return false;
+ }
+}
+
+module.exports = {
+ getPopupStyles: getPopupStyles,
+ onContentTouchMove: onContentTouchMove,
+};
From 696529fb932fa6a21b019876580a5da0b9a53aaa Mon Sep 17 00:00:00 2001
From: Wesley <985189328@qq.com>
Date: Wed, 12 Nov 2025 17:38:15 +0800
Subject: [PATCH 04/11] feat: demo
---
.../components/popover/_example/base/index.js | 22 +++++++
.../popover/_example/base/index.json | 7 +++
.../popover/_example/base/index.wxml | 62 +++++++++++++++++++
.../popover/_example/base/index.wxss | 40 ++++++++++++
.../components/popover/_example/popover.json | 7 +++
.../components/popover/_example/popover.less | 4 ++
.../components/popover/_example/popover.ts | 1 +
.../components/popover/_example/popover.wxml | 7 +++
packages/components/popover/popover.json | 3 +-
packages/components/popover/popover.less | 2 +-
packages/components/popover/popover.wxml | 5 +-
11 files changed, 155 insertions(+), 5 deletions(-)
create mode 100644 packages/components/popover/_example/base/index.js
create mode 100644 packages/components/popover/_example/base/index.json
create mode 100644 packages/components/popover/_example/base/index.wxml
create mode 100644 packages/components/popover/_example/base/index.wxss
create mode 100644 packages/components/popover/_example/popover.json
create mode 100644 packages/components/popover/_example/popover.less
create mode 100644 packages/components/popover/_example/popover.ts
create mode 100644 packages/components/popover/_example/popover.wxml
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..ca4984243
--- /dev/null
+++ b/packages/components/popover/_example/base/index.wxml
@@ -0,0 +1,62 @@
+
+
+
+ 弹出气泡内容
+
+
+ 带箭头
+
+
+
+
+
+
+ 不带箭头
+
+
+
+
+
+
+ 选项{{ 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/popover.json b/packages/components/popover/_example/popover.json
new file mode 100644
index 000000000..a1e612bf1
--- /dev/null
+++ b/packages/components/popover/_example/popover.json
@@ -0,0 +1,7 @@
+{
+ "navigationBarTitleText": "Popover",
+ "navigationBarBackgroundColor": "#fff",
+ "usingComponents": {
+ "base": "./base"
+ }
+}
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..c4a3c6563
--- /dev/null
+++ b/packages/components/popover/_example/popover.wxml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/packages/components/popover/popover.json b/packages/components/popover/popover.json
index babc8a1f1..3b3501e30 100644
--- a/packages/components/popover/popover.json
+++ b/packages/components/popover/popover.json
@@ -2,7 +2,6 @@
"component": true,
"styleIsolation": "apply-shared",
"usingComponents": {
- "t-overlay": "../overlay/overlay",
- "t-icon": "../icon/icon"
+ "t-overlay": "../overlay/overlay"
}
}
diff --git a/packages/components/popover/popover.less b/packages/components/popover/popover.less
index fb408dfcc..016131315 100644
--- a/packages/components/popover/popover.less
+++ b/packages/components/popover/popover.less
@@ -3,7 +3,7 @@
@popover: ~'@{prefix}-popover';
// 主题色变量
-@popover-padding: 24rpx;
+@popover-padding: var(--td-popover-padding, 24rpx);
@popover-arrow: 12rpx;
.@{popover}__wrapper {
diff --git a/packages/components/popover/popover.wxml b/packages/components/popover/popover.wxml
index d1a4f916e..0c4e0b6e0 100644
--- a/packages/components/popover/popover.wxml
+++ b/packages/components/popover/popover.wxml
@@ -1,7 +1,7 @@
-
-
+
+
@@ -26,6 +26,7 @@
>
+ {{content}}
From 50e93fe40c7511bd1c1d996517028bd7184c790d Mon Sep 17 00:00:00 2001
From: Wesley <985189328@qq.com>
Date: Thu, 13 Nov 2025 12:00:51 +0800
Subject: [PATCH 05/11] feat: demo
---
.../popover/_example/base/index.wxml | 29 +-
.../popover/_example/placement/index.js | 31 ++
.../popover/_example/placement/index.json | 7 +
.../popover/_example/placement/index.wxml | 299 ++++++++++++++++++
.../popover/_example/placement/index.wxss | 43 +++
.../components/popover/_example/popover.json | 4 +-
.../components/popover/_example/popover.wxml | 6 +
.../popover/_example/theme/index.js | 25 ++
.../popover/_example/theme/index.json | 7 +
.../popover/_example/theme/index.wxml | 130 ++++++++
.../popover/_example/theme/index.wxss | 22 ++
packages/components/popover/popover.less | 264 +++++++++++-----
packages/components/popover/popover.ts | 94 ++++--
packages/components/popover/popover.wxml | 12 +-
14 files changed, 846 insertions(+), 127 deletions(-)
create mode 100644 packages/components/popover/_example/placement/index.js
create mode 100644 packages/components/popover/_example/placement/index.json
create mode 100644 packages/components/popover/_example/placement/index.wxml
create mode 100644 packages/components/popover/_example/placement/index.wxss
create mode 100644 packages/components/popover/_example/theme/index.js
create mode 100644 packages/components/popover/_example/theme/index.json
create mode 100644 packages/components/popover/_example/theme/index.wxml
create mode 100644 packages/components/popover/_example/theme/index.wxss
diff --git a/packages/components/popover/_example/base/index.wxml b/packages/components/popover/_example/base/index.wxml
index ca4984243..107aa4a34 100644
--- a/packages/components/popover/_example/base/index.wxml
+++ b/packages/components/popover/_example/base/index.wxml
@@ -10,17 +10,10 @@
>
弹出气泡内容
-
+
带箭头
-
+
+
@@ -33,20 +26,14 @@
visible="{{visible.noArrow}}"
>
- 不带箭头
+
+ 不带箭头
+
+
-
+
选项{{ index + 1 }}
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
index a1e612bf1..2d42c5267 100644
--- a/packages/components/popover/_example/popover.json
+++ b/packages/components/popover/_example/popover.json
@@ -2,6 +2,8 @@
"navigationBarTitleText": "Popover",
"navigationBarBackgroundColor": "#fff",
"usingComponents": {
- "base": "./base"
+ "base": "./base",
+ "theme": "./theme",
+ "placement": "./placement"
}
}
diff --git a/packages/components/popover/_example/popover.wxml b/packages/components/popover/_example/popover.wxml
index c4a3c6563..477f6a013 100644
--- a/packages/components/popover/_example/popover.wxml
+++ b/packages/components/popover/_example/popover.wxml
@@ -4,4 +4,10 @@
+
+
+
+
+
+
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/popover.less b/packages/components/popover/popover.less
index 016131315..cc0491bed 100644
--- a/packages/components/popover/popover.less
+++ b/packages/components/popover/popover.less
@@ -4,7 +4,22 @@
// 主题色变量
@popover-padding: var(--td-popover-padding, 24rpx);
-@popover-arrow: 12rpx;
+@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;
@@ -38,133 +53,226 @@
height: 0;
border-style: solid;
border-color: transparent;
- border-width: @popover-arrow;
+ border-width: @popover-arrow-width;
}
// 主题
- &--dark {
- color: #fff;
- background: @font-gray-1;
+ .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();
- .@{popover}__arrow {
- color: @font-gray-1;
+.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;
}
}
+}
- &--light {
- color: @text-color-primary;
- background: @bg-color-container;
+.content-placement-bottom {
+ .@{prefix}-popover[data-placement^='bottom'] {
+ .@{prefix}-popover__content {
+ margin-top: @popover-content-margin;
+ }
+ }
+}
- .@{popover}__arrow {
- color: @bg-color-container;
+.content-placement-left {
+ .@{prefix}-popover[data-placement^='left'] {
+ .@{prefix}-popover__content {
+ margin-right: @popover-content-margin;
}
}
+}
- &--brand {
- color: @primary-color-7;
- background: @primary-color-1;
+.content-placement-right {
+ .@{prefix}-popover[data-placement^='right'] {
+ .@{prefix}-popover__content {
+ margin-left: @popover-content-margin;
+ }
+ }
+}
- .@{popover}__arrow {
- color: @primary-color-1;
+.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);
}
}
- &--success {
- color: @success-color-5;
- background: @success-color-1;
+ .@{prefix}-popover[data-placement='top'] {
+ transform-origin: 50% 100%;
- .@{popover}__arrow {
- color: @success-color-1;
+ .@{prefix}-popover__arrow {
+ left: 50%;
+ transform: translateX(-50%);
}
}
- &--warning {
- color: @warning-color-5;
- background: @warning-color-1;
+ .@{prefix}-popover[data-placement='top-start'] {
+ transform-origin: 0 100%;
- .@{popover}__arrow {
- color: @warning-color-1;
+ .@{prefix}-popover__arrow {
+ left: @popover-padding;
}
}
- &--error {
- color: @error-color-6;
- background: @error-color-1;
+ .@{prefix}-popover[data-placement='top-end'] {
+ transform-origin: 100% 100%;
- .@{popover}__arrow {
- color: @error-color-1;
+ .@{prefix}-popover__arrow {
+ right: @popover-padding;
}
}
+}
- // 箭头方向与偏移
- &[data-placement^='top'] {
- transform-origin: 50% 100%;
- .@{popover}__content {
- margin-bottom: 16rpx;
+.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);
}
- .@{popover}__arrow {
- bottom: 0;
- left: 50%;
- transform: translateX(-50%);
- border-bottom-width: 0;
- border-top-color: currentColor;
- margin-bottom: -@popover-arrow;
+ }
+
+ .@{prefix}-popover[data-placement='left'] {
+ transform-origin: 100% 50%;
+
+ .@{prefix}-popover__arrow {
+ top: 50%;
+ transform: translateY(-50%);
}
}
- &[data-placement^='bottom'] {
- transform-origin: 50% 0;
- .@{popover}__content {
- margin-top: 16rpx;
+ .@{prefix}-popover[data-placement='left-start'] {
+ transform-origin: 100% 0;
+
+ .@{prefix}-popover__arrow {
+ top: @popover-padding;
}
- .@{popover}__arrow {
+ }
+
+ .@{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;
- left: 50%;
- transform: translateX(-50%);
border-top-width: 0;
border-bottom-color: currentColor;
- margin-top: -@popover-arrow;
+ margin-top: calc(@popover-arrow-width * -1);
}
}
- &[data-placement^='left'] {
- transform-origin: 100% 50%;
- .@{popover}__content {
- margin-right: 16rpx;
+ .@{prefix}-popover[data-placement='bottom'] {
+ transform-origin: 50% 0;
+
+ .@{prefix}-popover__arrow {
+ left: 50%;
+ transform: translateX(-50%);
}
- .@{popover}__arrow {
- right: 0;
- top: 50%;
- transform: translateY(-50%);
- border-right-width: 0;
- border-left-color: currentColor;
- margin-right: -@popover-arrow;
+ }
+
+ .@{prefix}-popover[data-placement='bottom-start'] {
+ transform-origin: 0 0;
+
+ .@{prefix}-popover__arrow {
+ left: @popover-padding;
}
}
- &[data-placement^='right'] {
- transform-origin: 0 50%;
- .@{popover}__content {
- margin-left: 16rpx;
+ .@{prefix}-popover[data-placement='bottom-end'] {
+ transform-origin: 100% 0;
+
+ .@{prefix}-popover__arrow {
+ right: @popover-padding;
}
- .@{popover}__arrow {
+ }
+}
+
+.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%);
- border-left-width: 0;
- border-right-color: currentColor;
- margin-left: -@popover-arrow;
}
}
- &.@{prefix}-fade-enter-to {
- opacity: 1;
- visibility: visible;
+ .@{prefix}-popover[data-placement='right-start'] {
+ transform-origin: 0 0;
+
+ .@{prefix}-popover__arrow {
+ top: @popover-padding;
+ }
}
- &.@{prefix}-fade-enter,
- &.@{prefix}-fade-leave-to {
- opacity: 0;
- visibility: hidden;
+ .@{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
index 34092b76b..f996e5489 100644
--- a/packages/components/popover/popover.ts
+++ b/packages/components/popover/popover.ts
@@ -1,3 +1,4 @@
+import { getWindowInfo } from 'tdesign-miniprogram/common/wechat';
import { TdPopoverProps } from './type';
import { SuperComponent, wxComponent } from '../common/src/index';
import config from '../common/config';
@@ -29,15 +30,16 @@ export default class Popover extends SuperComponent {
prefix,
classPrefix: name,
placement: 'top',
+ _placement: 'top',
theme: 'dark',
contentStyle: '',
- _triggerRect: null as WechatMiniprogram.BoundingClientRectCallbackResult | null,
+ arrowStyle: '',
};
observers = {
- visible(v: boolean) {
- if (v === undefined || v === null) return;
- this.updateVisible(v, 'prop');
+ visible(val: boolean) {
+ if (val === undefined || val === null) return;
+ this.updateVisible(val);
},
placement() {
if (this.data.realVisible) this.computePosition();
@@ -52,44 +54,91 @@ export default class Popover extends SuperComponent {
lifetimes = {
attached() {
if (this.properties.defaultVisible) {
- this.updateVisible(true, 'default');
+ this.updateVisible(true);
}
},
};
methods = {
- updateVisible(visible: boolean, trigger: string) {
+ updateVisible(visible: boolean) {
if (visible === this.data.visible) return;
this.setData({ visible }, () => {
- this.triggerEvent('visible-change', { visible, trigger });
+ this.triggerEvent('visible-change', { visible });
});
},
- onToggle() {
- const { realVisible } = this.data;
- this.updateVisible(!realVisible, 'trigger');
- },
-
onOverlayTap() {
if (this.properties.closeOnClickOutside) {
- this.updateVisible(false, 'overlay');
+ this.updateVisible(false);
}
},
+ // getPopperPlacement = (placement: TdPopoverProps['placement']): Placement => {
+ // return placement?.replace(/-(left|top)$/, '-start').replace(/-(right|bottom)$/, '-end') as Placement;
+ // },
+
+ // getPopoverOptions = () => ({
+ // placement: getPopperPlacement(props.placement),
+ // modifiers: [
+ // {
+ // name: 'arrow',
+ // options: {
+ // padding: placementPadding,
+ // },
+ // },
+ // ],
+ // }),
+
+ // 计算箭头偏移的样式:仅作用于箭头元素,不修改内容 padding
+ 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() {
// 计算触发元素和内容尺寸,设置 contentStyle
- // 简化:仅处理四个基础方向 top/right/bottom/left 以及带 start/end 的 12 种。
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] = res as [
- WechatMiniprogram.BoundingClientRectCallbackResult,
- WechatMiniprogram.BoundingClientRectCallbackResult,
- ];
+ const [triggerRect, contentRect, viewportOffset] = res;
if (!triggerRect || !contentRect) return;
- const offset = unitConvert(8); // 间距 8rpx => px
+
+ // 间距 8rpx => px
+ const offset = unitConvert(8);
let top = 0;
let left = 0;
@@ -134,8 +183,13 @@ export default class Popover extends SuperComponent {
}
}
+ const { scrollTop = 0, scrollLeft = 0 } = viewportOffset;
+ top += scrollTop;
+ left += scrollLeft;
+
const style = `top:${Math.max(top, 0)}px;left:${Math.max(left, 0)}px;`;
- this.setData({ contentStyle: style, _triggerRect: triggerRect });
+ 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
index 0c4e0b6e0..6054d51ed 100644
--- a/packages/components/popover/popover.wxml
+++ b/packages/components/popover/popover.wxml
@@ -1,10 +1,9 @@
-
+
-
-
- {{content}}
-
-
+ {{content}}
+
+
From e6dc281f5264a2785e917cd265ef50cee2314f04 Mon Sep 17 00:00:00 2001
From: Wesley <985189328@qq.com>
Date: Thu, 13 Nov 2025 13:33:29 +0800
Subject: [PATCH 06/11] feat: demo and test
---
.../__test__/__snapshots__/demo.test.js.snap | 740 ++++++++++++++++++
.../components/popover/__test__/demo.test.js | 19 +
.../popover/_example/base/index.wxml | 4 +-
packages/components/popover/popover.ts | 81 +-
packages/components/popover/popover.wxml | 11 +-
packages/components/popover/popover.wxs | 18 -
6 files changed, 799 insertions(+), 74 deletions(-)
create mode 100644 packages/components/popover/__test__/__snapshots__/demo.test.js.snap
create mode 100644 packages/components/popover/__test__/demo.test.js
delete mode 100644 packages/components/popover/popover.wxs
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..461af539d
--- /dev/null
+++ b/packages/components/popover/__test__/__snapshots__/demo.test.js.snap
@@ -0,0 +1,740 @@
+// 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.wxml b/packages/components/popover/_example/base/index.wxml
index 107aa4a34..c87f61b08 100644
--- a/packages/components/popover/_example/base/index.wxml
+++ b/packages/components/popover/_example/base/index.wxml
@@ -16,7 +16,7 @@
-
+
-
+
diff --git a/packages/components/popover/popover.ts b/packages/components/popover/popover.ts
index f996e5489..10b7ada13 100644
--- a/packages/components/popover/popover.ts
+++ b/packages/components/popover/popover.ts
@@ -73,23 +73,11 @@ export default class Popover extends SuperComponent {
}
},
- // getPopperPlacement = (placement: TdPopoverProps['placement']): Placement => {
- // return placement?.replace(/-(left|top)$/, '-start').replace(/-(right|bottom)$/, '-end') as Placement;
- // },
-
- // getPopoverOptions = () => ({
- // placement: getPopperPlacement(props.placement),
- // modifiers: [
- // {
- // name: 'arrow',
- // options: {
- // padding: placementPadding,
- // },
- // },
- // ],
- // }),
-
- // 计算箭头偏移的样式:仅作用于箭头元素,不修改内容 padding
+ onWrapperTap() {
+ const curr = !!this.data.visible;
+ this.updateVisible(!curr);
+ },
+
calcArrowStyle(placement: string, contentDom: any, popoverDom: any) {
const horizontal = ['top', 'bottom'];
const vertical = ['left', 'right'];
@@ -124,7 +112,6 @@ export default class Popover extends SuperComponent {
},
async computePosition() {
- // 计算触发元素和内容尺寸,设置 contentStyle
const { placement } = this.data;
const _placement = placement.replace(/-(left|top)$/, '-start').replace(/-(right|bottom)$/, '-end');
this.setData({ _placement });
@@ -137,49 +124,49 @@ export default class Popover extends SuperComponent {
const [triggerRect, contentRect, viewportOffset] = res;
if (!triggerRect || !contentRect) return;
- // 间距 8rpx => px
const offset = unitConvert(8);
let top = 0;
let left = 0;
- const base = placement.split('-')[0];
- const second = placement.split('-')[1];
-
- switch (base) {
- case 'top':
- top = triggerRect.top - contentRect.height - offset;
- break;
- case 'bottom':
- top = triggerRect.top + triggerRect.height + offset;
- break;
- case 'left':
- left = triggerRect.left - contentRect.width - offset;
- break;
- case 'right':
- left = triggerRect.left + triggerRect.width + offset;
- break;
- default:
- top = triggerRect.top - contentRect.height - offset;
+ 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 (['top', 'bottom'].includes(base)) {
- if (!second) {
- left = triggerRect.left + triggerRect.width / 2 - contentRect.width / 2;
- } else if (second === 'left') {
+ if (isTopBase || isBottomBase) {
+ if (isStart) {
left = triggerRect.left;
- } else if (second === 'right') {
+ } else if (isEnd) {
left = triggerRect.left + triggerRect.width - contentRect.width;
+ } else {
+ left = triggerRect.left + triggerRect.width / 2 - contentRect.width / 2;
}
}
+
// 水平方向的垂直居中/偏移
- if (['left', 'right'].includes(base)) {
- if (!second) {
- top = triggerRect.top + triggerRect.height / 2 - contentRect.height / 2;
- } else if (second === 'top') {
+ if (isLeftBase || isRightBase) {
+ if (isStart) {
top = triggerRect.top;
- } else if (second === 'bottom') {
+ } else if (isEnd) {
top = triggerRect.top + triggerRect.height - contentRect.height;
+ } else {
+ top = triggerRect.top + triggerRect.height / 2 - contentRect.height / 2;
}
}
diff --git a/packages/components/popover/popover.wxml b/packages/components/popover/popover.wxml
index 6054d51ed..d980312a6 100644
--- a/packages/components/popover/popover.wxml
+++ b/packages/components/popover/popover.wxml
@@ -1,6 +1,4 @@
-
-
-
+
@@ -17,12 +15,11 @@
-
+
{{content}}
diff --git a/packages/components/popover/popover.wxs b/packages/components/popover/popover.wxs
deleted file mode 100644
index 2f4a01f01..000000000
--- a/packages/components/popover/popover.wxs
+++ /dev/null
@@ -1,18 +0,0 @@
-function getPopupStyles(zIndex, distanceTop, placement) {
- var zIndexStyle = zIndex ? 'z-index:' + zIndex + ';' : '';
- if ((placement === 'top' || placement === 'left' || placement === 'right') && distanceTop) {
- zIndexStyle = zIndexStyle + 'top:' + distanceTop + 'px;' + '--td-popup-distance-top:' + distanceTop + 'px;';
- }
- return zIndexStyle;
-}
-
-function onContentTouchMove(e) {
- if (e.target && e.target.dataset.prevention) {
- return false;
- }
-}
-
-module.exports = {
- getPopupStyles: getPopupStyles,
- onContentTouchMove: onContentTouchMove,
-};
From b966a17868b292790a9fdec2c49b0f4434a7df9a Mon Sep 17 00:00:00 2001
From: Wesley <985189328@qq.com>
Date: Thu, 13 Nov 2025 13:53:25 +0800
Subject: [PATCH 07/11] feat: test
---
.../components/popover/__test__/index.test.js | 60 +++++++++++++++++++
1 file changed, 60 insertions(+)
create mode 100644 packages/components/popover/__test__/index.test.js
diff --git a/packages/components/popover/__test__/index.test.js b/packages/components/popover/__test__/index.test.js
new file mode 100644
index 000000000..3d4d4cb01
--- /dev/null
+++ b/packages/components/popover/__test__/index.test.js
@@ -0,0 +1,60 @@
+import path from 'path';
+import simulate from 'miniprogram-simulate';
+
+describe('popover', () => {
+ const popoverId = load(path.resolve(__dirname, '../popover'), 't-popover');
+
+ it('renders content slot & toggles visible', async () => {
+ const id = simulate.load({
+ template: `contenttrigger`,
+ usingComponents: { 't-popover': popoverId },
+ });
+
+ const comp = simulate.render(id);
+ comp.attach(document.createElement('parent-wrapper'));
+
+ if (!VIRTUAL_HOST) {
+ // 初始不可见
+ expect(comp.querySelector('#p >>> .t-popover__content')).toBeUndefined();
+ // 点击触发
+ const $trigger = comp.querySelector('#p >>> .t-popover__wrapper');
+ $trigger.dispatchEvent('tap');
+ await simulate.sleep(0);
+ expect(comp.querySelector('#p >>> .t-popover__content')).toBeTruthy();
+ }
+ });
+
+ it('close on outside tap', async () => {
+ const fn = jest.fn();
+ const id = simulate.load({
+ template: 'content',
+ usingComponents: {
+ 't-popover': popoverId,
+ },
+ data: {
+ visible: true,
+ },
+ methods: {
+ onClose: fn,
+ },
+ });
+ const comp = simulate.render(id);
+ comp.attach(document.createElement('parent-wrapper'));
+
+ if (!VIRTUAL_HOST) {
+ const $trigger = comp.querySelector('#p >>> .t-popover__wrapper');
+ $trigger.dispatchEvent('tap');
+ await simulate.sleep(0);
+
+ const $overlay = comp.querySelector('#p >>> #popover-overlay');
+ expect($overlay).toBeTruthy();
+
+ $overlay.dispatchEvent('tap');
+ await simulate.sleep(0);
+
+ expect(fn).toHaveBeenCalledTimes(1);
+ comp.setData({ visible: false });
+ expect(comp.querySelector('#p >>> .t-popover__content')).toBeUndefined();
+ }
+ });
+});
From ac9f0e76695e4a836c5640ab5e0805d3c45990db Mon Sep 17 00:00:00 2001
From: Wesley <985189328@qq.com>
Date: Thu, 13 Nov 2025 16:20:17 +0800
Subject: [PATCH 08/11] feat: test
---
.../popover/__test__/__snapshots__/demo.test.js.snap | 2 ++
packages/components/popover/_example/base/index.wxml | 9 ++++++++-
packages/components/popover/popover.ts | 4 +---
3 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/packages/components/popover/__test__/__snapshots__/demo.test.js.snap b/packages/components/popover/__test__/__snapshots__/demo.test.js.snap
index 461af539d..951476e00 100644
--- a/packages/components/popover/__test__/__snapshots__/demo.test.js.snap
+++ b/packages/components/popover/__test__/__snapshots__/demo.test.js.snap
@@ -52,6 +52,7 @@ exports[`Popover Popover base demo works fine 1`] = `
showArrow="{{false}}"
theme="dark"
visible="{{false}}"
+ bind:visible-change="onVisibleChange"
>
@@ -33,7 +34,13 @@
-
+
选项{{ index + 1 }}
diff --git a/packages/components/popover/popover.ts b/packages/components/popover/popover.ts
index 10b7ada13..f0daa0192 100644
--- a/packages/components/popover/popover.ts
+++ b/packages/components/popover/popover.ts
@@ -29,9 +29,7 @@ export default class Popover extends SuperComponent {
data = {
prefix,
classPrefix: name,
- placement: 'top',
_placement: 'top',
- theme: 'dark',
contentStyle: '',
arrowStyle: '',
};
@@ -61,7 +59,7 @@ export default class Popover extends SuperComponent {
methods = {
updateVisible(visible: boolean) {
- if (visible === this.data.visible) return;
+ if (visible === this.data.realVisible) return;
this.setData({ visible }, () => {
this.triggerEvent('visible-change', { visible });
});
From 51f8d1ae5206705c2127a55907b9f6616ae89a7c Mon Sep 17 00:00:00 2001
From: Wesley <985189328@qq.com>
Date: Thu, 13 Nov 2025 16:49:46 +0800
Subject: [PATCH 09/11] chore: test
---
packages/components/popover/__test__/index.test.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/packages/components/popover/__test__/index.test.js b/packages/components/popover/__test__/index.test.js
index 3d4d4cb01..238ba6141 100644
--- a/packages/components/popover/__test__/index.test.js
+++ b/packages/components/popover/__test__/index.test.js
@@ -16,10 +16,12 @@ describe('popover', () => {
if (!VIRTUAL_HOST) {
// 初始不可见
expect(comp.querySelector('#p >>> .t-popover__content')).toBeUndefined();
+
// 点击触发
const $trigger = comp.querySelector('#p >>> .t-popover__wrapper');
$trigger.dispatchEvent('tap');
await simulate.sleep(0);
+
expect(comp.querySelector('#p >>> .t-popover__content')).toBeTruthy();
}
});
From 995170f509e489ec7fe4dd237731d9dc24b88b25 Mon Sep 17 00:00:00 2001
From: Wesley <985189328@qq.com>
Date: Thu, 13 Nov 2025 17:11:41 +0800
Subject: [PATCH 10/11] fix: test
---
.../components/popover/__test__/index.test.js | 62 -------------------
1 file changed, 62 deletions(-)
delete mode 100644 packages/components/popover/__test__/index.test.js
diff --git a/packages/components/popover/__test__/index.test.js b/packages/components/popover/__test__/index.test.js
deleted file mode 100644
index 238ba6141..000000000
--- a/packages/components/popover/__test__/index.test.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import path from 'path';
-import simulate from 'miniprogram-simulate';
-
-describe('popover', () => {
- const popoverId = load(path.resolve(__dirname, '../popover'), 't-popover');
-
- it('renders content slot & toggles visible', async () => {
- const id = simulate.load({
- template: `contenttrigger`,
- usingComponents: { 't-popover': popoverId },
- });
-
- const comp = simulate.render(id);
- comp.attach(document.createElement('parent-wrapper'));
-
- if (!VIRTUAL_HOST) {
- // 初始不可见
- expect(comp.querySelector('#p >>> .t-popover__content')).toBeUndefined();
-
- // 点击触发
- const $trigger = comp.querySelector('#p >>> .t-popover__wrapper');
- $trigger.dispatchEvent('tap');
- await simulate.sleep(0);
-
- expect(comp.querySelector('#p >>> .t-popover__content')).toBeTruthy();
- }
- });
-
- it('close on outside tap', async () => {
- const fn = jest.fn();
- const id = simulate.load({
- template: 'content',
- usingComponents: {
- 't-popover': popoverId,
- },
- data: {
- visible: true,
- },
- methods: {
- onClose: fn,
- },
- });
- const comp = simulate.render(id);
- comp.attach(document.createElement('parent-wrapper'));
-
- if (!VIRTUAL_HOST) {
- const $trigger = comp.querySelector('#p >>> .t-popover__wrapper');
- $trigger.dispatchEvent('tap');
- await simulate.sleep(0);
-
- const $overlay = comp.querySelector('#p >>> #popover-overlay');
- expect($overlay).toBeTruthy();
-
- $overlay.dispatchEvent('tap');
- await simulate.sleep(0);
-
- expect(fn).toHaveBeenCalledTimes(1);
- comp.setData({ visible: false });
- expect(comp.querySelector('#p >>> .t-popover__content')).toBeUndefined();
- }
- });
-});
From 70428b72e362e5a4d054e7d3cb251d1a7ac33f33 Mon Sep 17 00:00:00 2001
From: Wesley <985189328@qq.com>
Date: Fri, 21 Nov 2025 23:38:00 +0800
Subject: [PATCH 11/11] chore: fix visible
---
.../components/popover/_example/base/index.wxml | 13 +++++++------
packages/components/popover/popover.ts | 10 ++--------
packages/components/popover/popover.wxml | 2 +-
3 files changed, 10 insertions(+), 15 deletions(-)
diff --git a/packages/components/popover/_example/base/index.wxml b/packages/components/popover/_example/base/index.wxml
index b7239e8f4..57a9e7db8 100644
--- a/packages/components/popover/_example/base/index.wxml
+++ b/packages/components/popover/_example/base/index.wxml
@@ -35,11 +35,12 @@
@@ -48,9 +49,9 @@
- 自定义内容
+
+ 自定义内容
+
+
diff --git a/packages/components/popover/popover.ts b/packages/components/popover/popover.ts
index f0daa0192..213b58275 100644
--- a/packages/components/popover/popover.ts
+++ b/packages/components/popover/popover.ts
@@ -1,4 +1,4 @@
-import { getWindowInfo } from 'tdesign-miniprogram/common/wechat';
+import { getWindowInfo } from '../common/wechat';
import { TdPopoverProps } from './type';
import { SuperComponent, wxComponent } from '../common/src/index';
import config from '../common/config';
@@ -6,7 +6,6 @@ import props from './props';
import { unitConvert } from '../common/utils';
import transition from '../mixins/transition';
-// 保留 visible 以支持受控用法
delete props.visible;
export interface PopoverProps extends TdPopoverProps {}
@@ -59,7 +58,7 @@ export default class Popover extends SuperComponent {
methods = {
updateVisible(visible: boolean) {
- if (visible === this.data.realVisible) return;
+ if (visible === this.data.visible) return;
this.setData({ visible }, () => {
this.triggerEvent('visible-change', { visible });
});
@@ -71,11 +70,6 @@ export default class Popover extends SuperComponent {
}
},
- onWrapperTap() {
- const curr = !!this.data.visible;
- this.updateVisible(!curr);
- },
-
calcArrowStyle(placement: string, contentDom: any, popoverDom: any) {
const horizontal = ['top', 'bottom'];
const vertical = ['left', 'right'];
diff --git a/packages/components/popover/popover.wxml b/packages/components/popover/popover.wxml
index d980312a6..f43885327 100644
--- a/packages/components/popover/popover.wxml
+++ b/packages/components/popover/popover.wxml
@@ -1,4 +1,4 @@
-
+