Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 | -
28 changes: 23 additions & 5 deletions packages/components/popover/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,29 @@ isComponent: true
</blockquote>

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

{{ base }}

## API
### 组件样式

#### 气泡主题
{{ theme }}

#### 气泡位置
{{ placement }}

## FAQ

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

```html
<t-popover fixed>
<view class="t-popover-wrapper--fixed" />
</t-popover>
```

## API

### Popover Props

Expand All @@ -40,6 +57,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 +73,8 @@ visible-change | `(visible: boolean)` | 确认框显示或隐藏时触发

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

### Popover External Classes

Expand All @@ -70,4 +88,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