Repository navigation
feat: Popover——锚在父节点上的浮动卡片,不抢焦点、放不下自动翻转 - #48
Conversation
注册流程里密码框聚焦时要在它上方浮出规则清单,盖住上面的内容而不推开布局。 锚点就是父节点(Compose Popup 的锚定方式),显隐跟 open,点外面只报 onDismiss; placement 首选一侧放不下就翻转,matchAnchorWidth 与锚点同宽。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThis PR introduces the Popover built-in end to end—from schema and generated bindings through Compose runtime implementation and documentation—and demonstrates it in the password form. The implementation uses a parent-anchored, non-focusable Popup with fade transitions, explicit Material styling, outside-dismiss reporting, optional anchor-width matching, and pure tested placement logic that flips and clamps within the window. Sequence diagram for password rule Popover interactionsequenceDiagram
actor User
participant PasswordField
participant Popover
participant ComposePopup
participant PageState
User->>PasswordField: onFocusChange
PasswordField->>PageState: setFocused
PageState->>Popover: open = focused
Popover->>ComposePopup: Popup(focusable = false)
ComposePopup-->>Popover: Follow parent anchor
User->>PasswordField: onChange
PasswordField->>PageState: setPassword
PageState-->>Popover: Update rule children
User->>ComposePopup: Tap outside
ComposePopup->>Popover: onDismissRequest
Popover->>PageState: dispatch onDismiss
Flow diagram for Popover placement and renderingflowchart TD
A[Popover receives open] --> B{open is true}
B -- No --> C[Do not render]
B -- Yes --> D[Measure parent anchor]
D --> E[Select preferred side]
E --> F{Preferred side fits}
F -- Yes --> G[Place with gap]
F -- No --> H{Other side fits}
H -- Yes --> I[Flip to other side]
H -- No --> J[Choose roomier side]
J --> K[Clamp both axes to window]
I --> K
G --> K
K --> L[Render non-focusable Popup]
L --> M[Fade in or fade out]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
There was a problem hiding this comment.
CodeSeer 审查
结论:批准(附 1 条建议)
摘要
本次改动新增了 Popover 的 schema、运行时注册、Compose 渲染和示例页面,并实现了基于父布局的上下翻转定位、非焦点 Popup 与淡入淡出。定位计算对常规带页边距的锚点能够保持卡片与锚点对齐。主要风险在于 matchAnchorWidth 与窗口同宽锚点组合时,给阴影预留的外层尺寸会使实际卡片越出窗口并被裁切。
整个 PR 至 84faaf5 · gpt-5.6-terra
| val onTop = if (above) fitsTop || !fitsBottom && roomierTop else !fitsBottom && (fitsTop || roomierTop) | ||
| val x = if (rtl) anchor.right - content.width else anchor.left | ||
| return IntOffset( | ||
| x.coerceIn(0, (window.width - content.width).coerceAtLeast(0)), |
There was a problem hiding this comment.
[建议] matchAnchorWidth 时 popupContentSize 包含 Surface 外侧的 8dp padding,宽度会比锚点多 16dp;但这里将外层窗口的 x 限制为非负。若锚点本身铺满窗口,外层 x 会被钳到 0,而实际 Surface 从 x=8 开始、仍保持锚点宽度,右侧会越过窗口 8dp 并被 Popup 裁切,违背两轴不出窗口。应在计算中按实际卡片边界处理阴影边距(例如允许外层为负边距,或在边缘缩减/调整阴影留白),并补充全宽锚点的测试。
There was a problem hiding this comment.
已修(6c94794)。根因是卡片外那圈 8dp 阴影边距:当初以为阴影会被 Popup 窗口裁掉才加的,双端实测不留边距阴影也完整,于是直接去掉。现在 popup 内容就是卡片本身,全宽锚点时卡片与锚点同宽、x 为 0,不再越出窗口;补了 aFullWidthAnchorGetsAFullWidthCardFlushWithTheEdges(LTR / RTL)。
There was a problem hiding this comment.
Hey - I've found 3 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt" line_range="579-580" />
<code_context>
+ val onTop = if (above) fitsTop || !fitsBottom && roomierTop else !fitsBottom && (fitsTop || roomierTop)
+ val x = if (rtl) anchor.right - content.width else anchor.left
+ return IntOffset(
+ x.coerceIn(0, (window.width - content.width).coerceAtLeast(0)),
+ (if (onTop) topY else bottomY).coerceIn(0, (window.height - content.height).coerceAtLeast(0)),
+ )
+}
</code_context>
<issue_to_address>
**Oversized popovers get clipped**
When popover content is larger than the popup window on either axis, `popoverOffset` clamps the card origin but leaves its size unchanged, so part of the card extends beyond the window and users see clipped or off-screen content.
Constrain popover content to the available window dimensions before applying the offset.
</issue_to_address>
### Comment 2
<location path="compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt" line_range="551" />
<code_context>
+ AnimatedVisibility(visibleState = visible, enter = fadeIn(), exit = fadeOut()) {
+ Surface(
+ // the popup window clips to its content: leave the shadow room to fall
+ modifier = Modifier.padding(POPOVER_SHADOW_ROOM).then(if (width != null) Modifier.width(width) else Modifier),
+ shape = MaterialTheme.shapes.medium,
+ // explicit: contentColorFor would guess from the container and can land on a muted colour
</code_context>
<issue_to_address>
**Full-width cards render too narrow**
When `matchAnchorWidth` is true and the anchor fills the available window width, `Modifier.padding(8.dp)` reduces the available width before `Modifier.width(width)` is applied, so the card is constrained to 16dp less than the measured anchor width; callers using a full-width anchor see a narrower card.
Account for the horizontal shadow padding in the outer width constraint, or avoid consuming horizontal width for the shadow margin.
</issue_to_address>
### Comment 3
<location path="compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt" line_range="539" />
<code_context>
+ object : PopupPositionProvider {
+ // the window is the card plus its margin all round: place that so the card itself lines up with the anchor
+ override fun calculatePosition(anchorBounds: IntRect, windowSize: IntSize, layoutDirection: LayoutDirection, popupContentSize: IntSize) =
+ popoverOffset(anchorBounds.inflate(margin), windowSize, popupContentSize, above, gap - 2 * margin, layoutDirection == LayoutDirection.Rtl)
+ }
+ }
</code_context>
<issue_to_address>
**Tied overflow picks the wrong side**
When neither side fits and the actual available space below exceeds the space above by less than 16px, `anchorBounds.inflate(margin)` expands both sides, but `popoverOffset` compares those expanded spaces in `roomierTop`; the 8px shadow margin biases the comparison toward above by 16px, so it chooses the smaller side for near-ties and the card overlaps more of the anchor or surrounding content.
Compare the actual anchor’s available space on each side, or compensate for the inflation when choosing the roomier side.
Also at `compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt:575`.
</issue_to_address>Sourcery assessment
Approval pending. 3 findings to address first.
Blocking findings: compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt:580, compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt:551, compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt:539
边距是误判阴影被 Popup 窗口裁掉加的,双端实测不留边距阴影也完整。 它让全宽锚点时卡片窄 16dp 或越出窗口,两侧都放不下时比较也偏向上方。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
CodeSeer 审查
结论:批准(附 1 条建议)
上轮意见:1 条,已处理 0 条,待处理 1 条
compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt:575[建议]matchAnchorWidth时popupContentSize包含Surface外侧的 8dp padding,宽度…
摘要
本次增量移除了 Popover 为阴影预留的内边距,并相应简化了位置计算,使全宽锚点能够让卡片贴齐窗口边缘。新增测试覆盖了全宽锚点在 LTR/RTL 下的定位。结合给出的代码,未发现此次增量引入的可确认问题。
一个宿主的注册流程逼出的内置组件:密码框聚焦时在它上方浮出规则清单,盖住上面的内容而不推开布局。随 0.14.0 一起发。
改动
Popover:open(必填)、placement(above / below,默认 below)、matchAnchorWidth;事件onDismiss;children 是卡片内容;layout: falsePopup本来就锚在调用处的父布局上,内置组件渲染 children 不加包装。写法是Box把锚元素和它包在一起popoverOffset:首选侧放得下用首选侧,否则另一侧,都放不下取空间大的一侧;起始边对齐(RTL 对齐右边);两轴不出窗口。滚动时跟随锚点PopupProperties(focusable = false):锚在聚焦输入框上不收键盘open,淡入淡出;点外面只报onDismiss不自己收(刚点的输入框也算外面)surface底、onSurface字(显式,不靠contentColorFor)、medium 圆角、3dp 阴影(双端 Popup 窗口都不裁阴影,不留边距)命名:M3 最接近的是 rich tooltip,但 M3 tooltip 由框架长按 / 悬停触发、偏短文字、宽度上限 320dp;这里是页面掌握开关、内容不限的锚定浮层,与 Web / iOS 的 popover 同义。
Tooltip留给将来按 M3 语义实现的那个。不做:箭头、背景色 prop、左右弹出。验证
pnpm schema:check/runtime:check/typecheck/test;:compose:testAndroidHostTest(新增PopoverOffsetTest六例,含全宽锚点;PageHostTest内置组件数 17 → 18)、:compose:apiCheck、:sample:shared:testAndroidHostTestform页的密码框改用Popover展示规则:Android 模拟器(Pixel 10)与 iOS 模拟器(iPhone 17)上,聚焦浮在上方且与输入框同宽、键盘不收、输入时规则逐条变色;上滑页面使上方放不下时翻到下方,并跟随滚动🤖 Generated with Claude Code
Summary by Sourcery
Add a non-focusable, anchor-relative Popover component for displaying floating content without affecting layout.
New Features:
Enhancements:
Documentation:
Tests: