diff --git a/packages/components/popover/README.en-US.md b/packages/components/popover/README.en-US.md new file mode 100644 index 000000000..6fbbc2ca3 --- /dev/null +++ b/packages/components/popover/README.en-US.md @@ -0,0 +1,44 @@ +:: 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 | \- + +### CSS Variables + +The component provides the following CSS variables, which can be used to customize styles. +Name | Default Value | Description +-- | -- | -- +--td-popover-padding | 24rpx | - diff --git a/packages/components/popover/README.md b/packages/components/popover/README.md new file mode 100644 index 000000000..7271f2960 --- /dev/null +++ b/packages/components/popover/README.md @@ -0,0 +1,68 @@ +--- +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 | 内容样式类 + +### CSS Variables + +组件提供了下列 CSS 变量,可用于自定义样式。 +名称 | 默认值 | 描述 +-- | -- | -- +--td-popover-padding | 24rpx | - \ No newline at end of file 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..af6d88ea3 --- /dev/null +++ b/packages/components/popover/__test__/__snapshots__/demo.test.js.snap @@ -0,0 +1,748 @@ +// 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..c5f8bc332 --- /dev/null +++ b/packages/components/popover/_example/base/index.js @@ -0,0 +1,23 @@ +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) { + const { target } = e.currentTarget.dataset; + this.setData({ + [`visible.${target}`]: 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..18913ff6b --- /dev/null +++ b/packages/components/popover/_example/base/index.json @@ -0,0 +1,8 @@ +{ + "component": true, + "styleIsolation": "apply-shared", + "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..a1a536490 --- /dev/null +++ b/packages/components/popover/_example/base/index.wxml @@ -0,0 +1,58 @@ + + 带箭头的弹出气泡 + + 弹出气泡内容 + + + 带箭头 + + + + + 不带箭头的弹出气泡 + + + + 不带箭头 + + + + + 自定义内容弹出气泡 + + + + + 选项{{ 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..0cc516327 --- /dev/null +++ b/packages/components/popover/_example/base/index.wxss @@ -0,0 +1,42 @@ +.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 { + display: flex; + justify-content: center; + --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..e71ad8196 --- /dev/null +++ b/packages/components/popover/_example/placement/index.js @@ -0,0 +1,32 @@ +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) { + const { target } = e.currentTarget.dataset; + this.setData({ + [`visible.${target}`]: 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..8b8aa9234 --- /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..a26238f8d --- /dev/null +++ b/packages/components/popover/_example/theme/index.js @@ -0,0 +1,26 @@ +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) { + const { target } = e.currentTarget.dataset; + this.setData({ + [`visible.${target}`]: 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..3a8fb3370 --- /dev/null +++ b/packages/components/popover/popover.less @@ -0,0 +1,291 @@ +@import '../common/style/base.less'; + +@popover: ~'@{prefix}-popover'; + +// 主题色变量 +@popover-padding: var(--td-popover-padding, 24rpx); +@popover-arrow-width: 16rpx; +@popover-content-margin: 16rpx; +@popover-content-arrow-margin: 24rpx; + +// 主题色变量 +@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; + } + .@{prefix}-popover__content--arrow { + margin-bottom: @popover-content-arrow-margin; + } + } +} + +.content-placement-bottom { + .@{prefix}-popover[data-placement^='bottom'] { + .@{prefix}-popover__content { + margin-top: @popover-content-margin; + } + .@{prefix}-popover__content--arrow { + margin-top: @popover-content-arrow-margin; + } + } +} + +.content-placement-left { + .@{prefix}-popover[data-placement^='left'] { + .@{prefix}-popover__content { + margin-right: @popover-content-margin; + } + .@{prefix}-popover__content--arrow { + margin-right: @popover-content-arrow-margin; + } + } +} + +.content-placement-right { + .@{prefix}-popover[data-placement^='right'] { + .@{prefix}-popover__content { + margin-left: @popover-content-margin; + } + .@{prefix}-popover__content--arrow { + margin-left: @popover-content-arrow-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..3d5f38f0b --- /dev/null +++ b/packages/components/popover/popover.ts @@ -0,0 +1,239 @@ +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 { debounce } from '../common/utils'; +import transition from '../mixins/transition'; +import pageScrollMixin from '../mixins/page-scroll'; + +delete props.visible; + +export interface PopoverProps extends TdPopoverProps {} + +const { prefix } = config; +const name = `${prefix}-popover`; + +@wxComponent() +export default class Popover extends SuperComponent { + behaviors = [transition(), pageScrollMixin()]; + + externalClasses = [`${prefix}-class`, `${prefix}-class-content`, `${prefix}-class-trigger`]; + + options = { + multipleSlots: true, + }; + + properties = props; + + data = { + prefix, + classPrefix: name, + _placement: 'top', + contentStyle: '', + arrowStyle: '', + }; + + controlledProps = [ + { + key: 'visible', + event: 'visible-change', + }, + ]; + + observers = { + visible(val: boolean) { + if (val === undefined || val === null) return; + this.updateVisible(val); + }, + 'placement, realVisible'(v: boolean) { + if (v) { + this.computePosition(); + } + }, + }; + + methods = { + onScroll() { + if (this.data.realVisible) { + debounce(() => this.computePosition(), 100); + } + }, + + updateVisible(visible: boolean) { + if (visible === this.data.visible) return; + this.setData({ visible }, () => { + this._trigger('visible-change', { visible }); + }); + }, + + onOverlayTap() { + if (this.properties.closeOnClickOutside) { + this.updateVisible(false); + } + }, + + getToward(placement: string) { + const horizontal = ['top', 'bottom']; + const vertical = ['left', 'right']; + const isHorizontal = horizontal.find((item) => placement.includes(item)); + const isVertical = vertical.find((item) => placement.includes(item)); + const isBase = [...horizontal, ...vertical].find((item) => item === placement); + const isEnd = placement.includes('end'); + return { + isHorizontal, + isVertical, + isBase, + isEnd, + }; + }, + + calcArrowStyle(placement: string, contentDom: any, popoverDom: any) { + const { isHorizontal, isVertical, isBase, isEnd } = this.getToward(placement); + + if (isBase) { + return ''; + } + + const { width, left } = contentDom; + const { width: popperWidth, height: popperHeight } = popoverDom; + const { windowWidth } = getWindowInfo(); + + if (isHorizontal) { + const padding = isEnd ? Math.min(width + left, popperWidth) : Math.min(windowWidth - left, popperWidth); + if (isEnd) { + return `left:${padding - 28}px;`; + } + return `right:${padding - 28}px;`; + } + if (isVertical) { + const offset = popperHeight - 28; + if (isEnd) { + return `top:${offset}px;`; + } + return `bottom:${offset}px;top:unset;`; + } + return ''; + }, + + calcContentPosition(placement: string, triggerRect: any, contentRect: any) { + 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; + } else if (isBottomBase) { + top = triggerRect.top + triggerRect.height; + } else if (isLeftBase) { + left = triggerRect.left - contentRect.width; + } else if (isRightBase) { + left = triggerRect.left + triggerRect.width; + } else { + top = triggerRect.top - contentRect.height; + } + + const isStart = placement.includes('start'); + const isEnd = placement.includes('end'); + let align: 'start' | 'end' | 'center'; + if (isStart) align = 'start'; + else if (isEnd) align = 'end'; + else align = 'center'; + + if (isTopBase || isBottomBase) { + left = this.alignCrossAxis(triggerRect.left, triggerRect.width, contentRect.width, align); + } + + if (isLeftBase || isRightBase) { + top = this.alignCrossAxis(triggerRect.top, triggerRect.height, contentRect.height, align); + } + + return { top, left }; + }, + + alignCrossAxis(start: number, triggerSize: number, contentSize: number, align: 'start' | 'end' | 'center') { + if (align === 'start') return start; + if (align === 'end') return start + triggerSize - contentSize; + return start + triggerSize / 2 - contentSize / 2; + }, + + calcPlacement(placement: string, triggerRect: any, contentRect: any) { + const { isHorizontal, isVertical } = this.getToward(placement); + // 获取内容大小 + const { width: contentWidth, height: contentHeight } = contentRect; + // 获取所在位置 + const { left: triggerLeft, top: triggerTop, right: triggerRight, bottom: triggerBottom } = triggerRect; + // 是否能正常放置 + let canPlace = true; + const { windowWidth, windowHeight } = getWindowInfo(); + let finalPlacement = placement; + + if (isHorizontal) { + if (placement.startsWith('top')) { + canPlace = triggerTop - contentHeight >= 0; + } else if (placement.startsWith('bottom')) { + canPlace = triggerBottom + contentHeight <= windowHeight; + } + } else if (isVertical) { + if (placement.startsWith('left')) { + canPlace = triggerLeft - contentWidth >= 0; + } else if (placement.startsWith('right')) { + canPlace = triggerRight + contentWidth <= windowWidth; + } + } + + if (!canPlace) { + // 反向 + if (isHorizontal) { + finalPlacement = placement.startsWith('top') + ? placement.replace('top', 'bottom') + : placement.replace('bottom', 'top'); + } else if (isVertical) { + finalPlacement = placement.startsWith('left') + ? placement.replace('left', 'right') + : placement.replace('right', 'left'); + } + } + + const basePos = this.calcContentPosition(finalPlacement, triggerRect, contentRect); + + return { + placement: finalPlacement, + ...basePos, + }; + }, + + 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 { placement: finalPlacement, ...basePos } = this.calcPlacement(_placement, triggerRect, contentRect); + // TODO 优化:滚动时可能导致箭头闪烁 + this.setData({ _placement: finalPlacement }); + + const { scrollTop = 0, scrollLeft = 0 } = viewportOffset; + const top = basePos.top + scrollTop; + const left = basePos.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..9651c2c13 --- /dev/null +++ b/packages/components/popover/popover.wxml @@ -0,0 +1,30 @@ + + + + + + + + + + {{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/pages/home/data/ux.ts b/packages/tdesign-miniprogram/example/pages/home/data/ux.ts index a10e5e295..4ec0cd055 100644 --- a/packages/tdesign-miniprogram/example/pages/home/data/ux.ts +++ b/packages/tdesign-miniprogram/example/pages/home/data/ux.ts @@ -69,6 +69,10 @@ const skylineUx = { name: 'Overlay', label: '遮罩层', }, + { + name: 'Popover', + label: '弹出气泡', + }, { name: 'Popup', label: '弹出层', 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 + + + +