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