Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/components/popover/README.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

## API


### Popover Props

name | type | default | description | required
Expand All @@ -11,6 +10,7 @@ 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
fixed | Boolean | false | \- | 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
Expand Down Expand Up @@ -41,4 +41,4 @@ t-class-content | \-
The component provides the following CSS variables, which can be used to customize styles.
Name | Default Value | Description
-- | -- | --
--td-popover-padding | 24rpx | -
--td-popover-padding | 24rpx | -
26 changes: 21 additions & 5 deletions packages/components/popover/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,27 @@ isComponent: true
</blockquote>

### 组件类型
带箭头的弹出气泡
#### 带箭头的弹出气泡

{{ base }}

## API
### 组件样式

{{ theme }}

{{ placement }}

## FAQ

如果使用场景为 `fixed`,除了需要显示指定 `fixed` 属性为 `true`,还需在触发元素的顶层添加`t-popover-wrapper--fixed` 类,用于定位触发元素。

```html
<scroll-view type="list" scroll-y bind:scroll="onScroll">
<t-color-picker id="ColorPicker" fixed />
</scroll-view>
```

## API

### Popover Props

Expand All @@ -40,6 +55,7 @@ style | Object | - | 样式 | N
custom-style | Object | - | 样式,一般用于开启虚拟化组件节点场景 | N
close-on-click-outside | Boolean | true | 是否在点击外部元素后关闭菜单 | N
content | String | - | 确认框内容 | N
fixed | Boolean | false | 如果 popover 是在一个 `position:fixed` 的区域,需要显式指定属性 fixed 为 true | 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
Expand All @@ -55,8 +71,8 @@ visible-change | `(visible: boolean)` | 确认框显示或隐藏时触发

名称 | 描述
-- | --
\- | 自定义 `` 显示内容
content \| 自定义 `content` 显示内容
\- | 默认插槽,作用同 `content` 插槽
content | 自定义 `content` 显示内容

### Popover External Classes

Expand All @@ -70,4 +86,4 @@ t-class-content | 内容样式类
组件提供了下列 CSS 变量,可用于自定义样式。
名称 | 默认值 | 描述
-- | -- | --
--td-popover-padding | 24rpx | -
--td-popover-padding | 24rpx | -
4 changes: 4 additions & 0 deletions packages/components/popover/popover.less
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
overflow: visible;
transition: 0.2s ease-in-out all;

&--fixed {
position: fixed;
}

&__content {
position: relative;
padding: @popover-padding;
Expand Down
118 changes: 68 additions & 50 deletions packages/components/popover/popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,50 +161,59 @@ export default class Popover extends SuperComponent {
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,
};
calcPlacement(isFixed: boolean, placement: string, triggerRect: any, contentRect: any) {
return new Promise<{ placement: string; top: number; left: number }>((resolve) => {
// 选取当前组件节点所在的组件实例,以支持 fixed 定位的元素计算位置
const owner = this.selectOwnerComponent().createSelectorQuery();
owner.select(`.${name}-wrapper--fixed`).boundingClientRect();
Copy link
Collaborator

@Wesley-0808 Wesley-0808 Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

尝试过在本组件实例内获取这个元素,但无效,所以先获取了上层实例再获取元素
image

owner.exec((b) => {
const [triggerChildRect] = b;
if (triggerChildRect && isFixed) {
triggerRect = triggerChildRect;
}

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);

resolve({ placement: finalPlacement, ...basePos });
});
});
},

async computePosition() {
Expand All @@ -217,18 +226,27 @@ export default class Popover extends SuperComponent {
query.select(`#${name}-content`).boundingClientRect();

query.selectViewport().scrollOffset();
query.exec((res) => {
query.exec(async (res) => {
const [triggerRect, contentRect, viewportOffset] = res;
if (!triggerRect || !contentRect) return;

// 如果 fixed 定位,不需要加上滚动偏移量
const isFixed = this.properties.fixed;
// 最终放置位置
const { placement: finalPlacement, ...basePos } = this.calcPlacement(_placement, triggerRect, contentRect);
// TODO 优化:滚动时可能导致箭头闪烁
const { placement: finalPlacement, ...basePos } = await this.calcPlacement(
isFixed,
_placement,
triggerRect,
contentRect,
);

// TODO 优化:滚动时切换placement可能导致箭头闪烁
this.setData({ _placement: finalPlacement });

const { scrollTop = 0, scrollLeft = 0 } = viewportOffset;
const top = basePos.top + scrollTop;
const left = basePos.left + scrollLeft;
const { scrollTop = 0, scrollLeft = 0 } = viewportOffset || {};

const top = isFixed ? basePos.top : basePos.top + scrollTop;
const left = isFixed ? basePos.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);
Expand Down
2 changes: 1 addition & 1 deletion packages/components/popover/popover.wxml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
wx:if="{{realVisible}}"
id="{{classPrefix}}-content"
style="{{style}} {{contentStyle}} {{customStyle}}"
class="{{class}} {{classPrefix}} {{transitionClass}} {{prefix}}-class"
class="{{class}} {{classPrefix}} {{transitionClass}} {{prefix}}-class {{fixed ? classPrefix + '--fixed' : ''}}"
data-placement="{{_placement}}"
>
<view
Expand Down
5 changes: 5 additions & 0 deletions packages/components/popover/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ const props: TdPopoverProps = {
content: {
type: String,
},
/** 如果 popover 是在一个 `position:fixed` 的区域,需要显式指定属性 fixed 为 true */
fixed: {
type: Boolean,
value: false,
},
/** 浮层出现位置 */
placement: {
type: String,
Expand Down
8 changes: 8 additions & 0 deletions packages/components/popover/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export interface TdPopoverProps {
type: StringConstructor;
value?: string;
};
/**
* 如果 popover 是在一个 `position:fixed` 的区域,需要显式指定属性 fixed 为 true
* @default false
*/
fixed?: {
type: BooleanConstructor;
value?: boolean;
};
/**
* 浮层出现位置
* @default top
Expand Down
Loading