Skip to content

Commit 156f785

Browse files
CopilotWesley-0808anlyyao
authored
fix(popover): 修复 fixed 定位触发元素的气泡定位错误 (#4114)
* Initial plan * Fix popover positioning when trigger element has fixed position Co-authored-by: Wesley-0808 <[email protected]> * Add comment to document query result order Co-authored-by: Wesley-0808 <[email protected]> * feat: add fixed * fix: trigger fixed * chore: docs * docs(Popover): update fixed attribute description --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: Wesley-0808 <[email protected]> Co-authored-by: Wesley <[email protected]> Co-authored-by: anlyyao <[email protected]>
1 parent a383a2f commit 156f785

File tree

7 files changed

+100
-56
lines changed

7 files changed

+100
-56
lines changed

packages/components/popover/README.en-US.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
## API
44

5-
65
### Popover Props
76

87
name | type | default | description | required
@@ -11,6 +10,7 @@ style | Object | - | CSS(Cascading Style Sheets) | N
1110
custom-style | Object | - | CSS(Cascading Style Sheets),used to set style on virtual component | N
1211
close-on-click-outside | Boolean | true | \- | N
1312
content | String | - | \- | N
13+
fixed | Boolean | false | `1.12.1` | N
1414
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
1515
show-arrow | Boolean | true | \- | N
1616
theme | String | dark | options: dark/light/brand/success/warning/error | N

packages/components/popover/README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,21 @@ isComponent: true
2525
</blockquote>
2626

2727
### 组件类型
28-
带箭头的弹出气泡
28+
#### 带箭头的弹出气泡
2929

3030
{{ base }}
3131

32-
## API
32+
### 组件样式
33+
34+
#### 气泡主题
35+
{{ theme }}
36+
37+
#### 气泡位置
38+
{{ placement }}
3339

3440

41+
## API
42+
3543
### Popover Props
3644

3745
名称 | 类型 | 默认值 | 描述 | 必传
@@ -40,6 +48,7 @@ style | Object | - | 样式 | N
4048
custom-style | Object | - | 样式,一般用于开启虚拟化组件节点场景 | N
4149
close-on-click-outside | Boolean | true | 是否在点击外部元素后关闭菜单 | N
4250
content | String | - | 确认框内容 | N
51+
fixed | Boolean | false | `1.12.1`。如果触发元素为 `fixed` 场景,需要显示指定 `fixed` 属性为 `true`,同时需在触发元素层添加 `t-popover-wrapper--fixed` 类,用于定位触发元素 | N
4352
placement | String | top | 浮层出现位置。可选项:top/left/right/bottom/top-left/top-right/bottom-left/bottom-right/left-top/left-bottom/right-top/right-bottom | N
4453
show-arrow | Boolean | true | 是否显示浮层箭头 | N
4554
theme | String | dark | 弹出气泡主题。可选项:dark/light/brand/success/warning/error | N
@@ -55,8 +64,8 @@ visible-change | `(visible: boolean)` | 确认框显示或隐藏时触发
5564

5665
名称 | 描述
5766
-- | --
58-
\- | 自定义 `` 显示内容
59-
content \| 自定义 `content` 显示内容
67+
\- | 默认插槽,用于自定义触发元素
68+
content | 自定义 `content` 显示内容
6069

6170
### Popover External Classes
6271

packages/components/popover/popover.less

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
overflow: visible;
3333
transition: 0.2s ease-in-out all;
3434

35+
&--fixed {
36+
position: fixed;
37+
}
38+
3539
&__content {
3640
position: relative;
3741
padding: @popover-padding;

packages/components/popover/popover.ts

Lines changed: 68 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -161,50 +161,59 @@ export default class Popover extends SuperComponent {
161161
return start + triggerSize / 2 - contentSize / 2;
162162
},
163163

164-
calcPlacement(placement: string, triggerRect: any, contentRect: any) {
165-
const { isHorizontal, isVertical } = this.getToward(placement);
166-
// 获取内容大小
167-
const { width: contentWidth, height: contentHeight } = contentRect;
168-
// 获取所在位置
169-
const { left: triggerLeft, top: triggerTop, right: triggerRight, bottom: triggerBottom } = triggerRect;
170-
// 是否能正常放置
171-
let canPlace = true;
172-
const { windowWidth, windowHeight } = getWindowInfo();
173-
let finalPlacement = placement;
174-
175-
if (isHorizontal) {
176-
if (placement.startsWith('top')) {
177-
canPlace = triggerTop - contentHeight >= 0;
178-
} else if (placement.startsWith('bottom')) {
179-
canPlace = triggerBottom + contentHeight <= windowHeight;
180-
}
181-
} else if (isVertical) {
182-
if (placement.startsWith('left')) {
183-
canPlace = triggerLeft - contentWidth >= 0;
184-
} else if (placement.startsWith('right')) {
185-
canPlace = triggerRight + contentWidth <= windowWidth;
186-
}
187-
}
188-
189-
if (!canPlace) {
190-
// 反向
191-
if (isHorizontal) {
192-
finalPlacement = placement.startsWith('top')
193-
? placement.replace('top', 'bottom')
194-
: placement.replace('bottom', 'top');
195-
} else if (isVertical) {
196-
finalPlacement = placement.startsWith('left')
197-
? placement.replace('left', 'right')
198-
: placement.replace('right', 'left');
199-
}
200-
}
201-
202-
const basePos = this.calcContentPosition(finalPlacement, triggerRect, contentRect);
203-
204-
return {
205-
placement: finalPlacement,
206-
...basePos,
207-
};
164+
calcPlacement(isFixed: boolean, placement: string, triggerRect: any, contentRect: any) {
165+
return new Promise<{ placement: string; top: number; left: number }>((resolve) => {
166+
// 选取当前组件节点所在的组件实例,以支持 fixed 定位的元素计算位置
167+
const owner = this.selectOwnerComponent().createSelectorQuery();
168+
owner.select(`.${name}-wrapper--fixed`).boundingClientRect();
169+
owner.exec((b) => {
170+
const [triggerChildRect] = b;
171+
if (triggerChildRect && isFixed) {
172+
triggerRect = triggerChildRect;
173+
}
174+
175+
const { isHorizontal, isVertical } = this.getToward(placement);
176+
// 获取内容大小
177+
const { width: contentWidth, height: contentHeight } = contentRect;
178+
// 获取所在位置
179+
const { left: triggerLeft, top: triggerTop, right: triggerRight, bottom: triggerBottom } = triggerRect;
180+
// 是否能正常放置
181+
let canPlace = true;
182+
const { windowWidth, windowHeight } = getWindowInfo();
183+
let finalPlacement = placement;
184+
185+
if (isHorizontal) {
186+
if (placement.startsWith('top')) {
187+
canPlace = triggerTop - contentHeight >= 0;
188+
} else if (placement.startsWith('bottom')) {
189+
canPlace = triggerBottom + contentHeight <= windowHeight;
190+
}
191+
} else if (isVertical) {
192+
if (placement.startsWith('left')) {
193+
canPlace = triggerLeft - contentWidth >= 0;
194+
} else if (placement.startsWith('right')) {
195+
canPlace = triggerRight + contentWidth <= windowWidth;
196+
}
197+
}
198+
199+
if (!canPlace) {
200+
// 反向
201+
if (isHorizontal) {
202+
finalPlacement = placement.startsWith('top')
203+
? placement.replace('top', 'bottom')
204+
: placement.replace('bottom', 'top');
205+
} else if (isVertical) {
206+
finalPlacement = placement.startsWith('left')
207+
? placement.replace('left', 'right')
208+
: placement.replace('right', 'left');
209+
}
210+
}
211+
212+
const basePos = this.calcContentPosition(finalPlacement, triggerRect, contentRect);
213+
214+
resolve({ placement: finalPlacement, ...basePos });
215+
});
216+
});
208217
},
209218

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

219228
query.selectViewport().scrollOffset();
220-
query.exec((res) => {
229+
query.exec(async (res) => {
221230
const [triggerRect, contentRect, viewportOffset] = res;
222231
if (!triggerRect || !contentRect) return;
223232

233+
// 如果 fixed 定位,不需要加上滚动偏移量
234+
const isFixed = this.properties.fixed;
224235
// 最终放置位置
225-
const { placement: finalPlacement, ...basePos } = this.calcPlacement(_placement, triggerRect, contentRect);
226-
// TODO 优化:滚动时可能导致箭头闪烁
236+
const { placement: finalPlacement, ...basePos } = await this.calcPlacement(
237+
isFixed,
238+
_placement,
239+
triggerRect,
240+
contentRect,
241+
);
242+
243+
// TODO 优化:滚动时切换placement可能导致箭头闪烁
227244
this.setData({ _placement: finalPlacement });
228245

229-
const { scrollTop = 0, scrollLeft = 0 } = viewportOffset;
230-
const top = basePos.top + scrollTop;
231-
const left = basePos.left + scrollLeft;
246+
const { scrollTop = 0, scrollLeft = 0 } = viewportOffset || {};
247+
248+
const top = isFixed ? basePos.top : basePos.top + scrollTop;
249+
const left = isFixed ? basePos.left : basePos.left + scrollLeft;
232250

233251
const style = `top:${Math.max(top, 0)}px;left:${Math.max(left, 0)}px;`;
234252
const arrowStyle = this.calcArrowStyle(_placement, triggerRect, contentRect);

packages/components/popover/popover.wxml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
wx:if="{{realVisible}}"
1717
id="{{classPrefix}}-content"
1818
style="{{style}} {{contentStyle}} {{customStyle}}"
19-
class="{{class}} {{classPrefix}} {{transitionClass}} {{prefix}}-class"
19+
class="{{class}} {{classPrefix}} {{transitionClass}} {{prefix}}-class {{fixed ? classPrefix + '--fixed' : ''}}"
2020
data-placement="{{_placement}}"
2121
>
2222
<view

packages/components/popover/props.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ const props: TdPopoverProps = {
1515
content: {
1616
type: String,
1717
},
18+
/** 如果触发元素为 `fixed` 场景,需要显示指定 `fixed` 属性为 `true`,同时需在触发元素层添加 `t-popover-wrapper--fixed` 类,用于定位触发元素 */
19+
fixed: {
20+
type: Boolean,
21+
value: false,
22+
},
1823
/** 浮层出现位置 */
1924
placement: {
2025
type: String,

packages/components/popover/type.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ export interface TdPopoverProps {
2020
type: StringConstructor;
2121
value?: string;
2222
};
23+
/**
24+
* 如果触发元素为 `fixed` 场景,需要显示指定 `fixed` 属性为 `true`,同时需在触发元素层添加 `t-popover-wrapper--fixed` 类,用于定位触发元素
25+
* @default false
26+
*/
27+
fixed?: {
28+
type: BooleanConstructor;
29+
value?: boolean;
30+
};
2331
/**
2432
* 浮层出现位置
2533
* @default top

0 commit comments

Comments
 (0)