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 +
+ + + +

Popover

+
+
+
diff --git a/packages/tdesign-miniprogram/site/docs/overview.md b/packages/tdesign-miniprogram/site/docs/overview.md index 227b0b87c..4b0c7ba6e 100644 --- a/packages/tdesign-miniprogram/site/docs/overview.md +++ b/packages/tdesign-miniprogram/site/docs/overview.md @@ -447,6 +447,14 @@ spline: explain
+
+ + + +

Popover 弹出气泡

+
+
+
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 @@ - +