Skip to content

fix: use measurement clone to avoid disrupting popup animations during alignment#612

Open
AnthonyH16 wants to merge 1 commit intoreact-component:masterfrom
AnthonyH16:fix/neutralize-transform-during-align
Open

fix: use measurement clone to avoid disrupting popup animations during alignment#612
AnthonyH16 wants to merge 1 commit intoreact-component:masterfrom
AnthonyH16:fix/neutralize-transform-during-align

Conversation

@AnthonyH16
Copy link

@AnthonyH16 AnthonyH16 commented Mar 24, 2026

Problem

On Linux (X11 and Wayland), popup components (Popover, Tooltip, Dropdown, Select, DatePicker, etc.) are positioned wildly off-screen (e.g. top: -20000px) when they open.

Root Cause

The useAlign hook modifies the popup element's inline styles (left, top, transform, overflow) during measurement. On Linux, the alignment calculation runs mid-CSS-animation. This causes two issues:

  1. transition: all 1e-05s (set by antd CSS-in-JS during motion prepare phase) causes the browser to transition position values from their old state (-1000vw) instead of instantly applying the reset values (0), so getBoundingClientRect() reads the transitioning (old) position.

  2. transform: scale() (from entrance animation) distorts getBoundingClientRect() dimensions while getComputedStyle() returns untransformed values, producing incorrect scale factors (e.g. 0.1 instead of 1) that multiply offsets by 10x.

On macOS/Windows, different compositor frame scheduling means the alignment happens to run after animations settle, masking this issue.

Reproduction

  • Linux (any distro), X11 or Wayland, any Chromium-based browser or Firefox
  • Open any Ant Design component that uses a popup (Popover, Tooltip, Dropdown, Select, etc.)
  • The popup renders off-screen with massive negative top/left values

Solution

Replace the direct popup style manipulation with a shallow clone (cloneNode(false)) used as a measurement proxy:

  • The clone inherits the popup's classes and attributes (needed for correct box model)
  • transform: none and transition: none are set on the clone only, not the original popup
  • Position measurements use the clone's getBoundingClientRect() instead of the popup's
  • The original popup element is never modified during measurement, fully preserving CSS animations

This is a net reduction in code (-38 lines, +47 lines) since the save/restore logic for originLeft, originTop, originRight, originBottom, originOverflow, and the placeholder element are all eliminated.

Test Plan

  • All 130 existing tests pass (17/17 suites)
  • New test verifies popup styles are not modified during alignment
  • Verified on Linux (CachyOS, KDE Plasma 6.5.5, X11 and Wayland)
  • Verified zoom entrance animation is preserved on Linux
  • Verified correct positioning for Popover, Tooltip, Dropdown
  • Verified no visual change on macOS

Summary by CodeRabbit

发布说明

  • Bug 修复

    • 优化弹出层对齐测量:临时克隆一个不可见、不可交互的测量元素读取尺寸与位置(包括宽高与边界),避免修改原弹出层的行内样式,减少对动画、变换和布局的干扰,提升对齐稳定性。
  • 测试

    • 新增对齐单元测试,验证测量过程不会修改弹出层的关键样式属性(left/top/transform/transition/overflow),增强回归保护。

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical cross-platform rendering bug affecting popup components on Linux, where they would appear off-screen. The core problem stemmed from the alignment logic interfering with active CSS animations during measurement. The solution introduces a robust mechanism using a temporary, invisible clone of the popup for all measurement operations, thereby isolating the original element from disruptive style manipulations and preserving its animations. This change not only fixes the visual glitch but also streamlines the alignment code by removing redundant style management.

Highlights

  • Linux Popup Positioning Fix: Resolved an issue where popup components (Popover, Tooltip, Dropdown, etc.) were positioned wildly off-screen on Linux (X11 and Wayland) due to incorrect alignment calculations during CSS animations.
  • Measurement Proxy Implementation: Implemented a solution that uses a shallow clone of the popup element as a temporary measurement proxy. This clone inherits necessary styles but has its transform and transition properties reset, preventing interference with active CSS animations on the original popup.
  • Avoided Direct Style Manipulation: Eliminated direct modification of the original popup element's inline styles (left, top, transform, overflow) during the alignment measurement phase, which previously caused getBoundingClientRect() to return incorrect values.
  • Code Simplification: Reduced overall code complexity by removing the need for saving and restoring original style properties and the use of a separate placeholder element, resulting in a net reduction of lines of code.
  • New Test Coverage: Added a new test case to explicitly verify that popup styles are not modified during the alignment measurement process, ensuring the fix's integrity.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link

coderabbitai bot commented Mar 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9ce35c6f-c034-4955-aa41-12c21bc677a4

📥 Commits

Reviewing files that changed from the base of the PR and between 89469ce and 1543cec.

📒 Files selected for processing (2)
  • src/hooks/useAlign.ts
  • tests/align.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/align.test.tsx

Walkthrough

将对齐测量从直接修改弹出层样式改为在弹出层父容器中创建一个不可交互、隐藏的克隆元素(measureEl)进行测量;所有 getBoundingClientRect() 读取从该克隆元素获取,测量完成后移除克隆元素,不再恢复或修改原弹出层样式。测试新增断言以验证未改动 popup 的样式属性。

Changes

Cohort / File(s) Summary
对齐测量实现
src/hooks/useAlign.ts
移除对 placeholderElement 与对原 popupElement.style 的临时修改;新增 measureEl = popup.cloneNode(false),将其设置为隐藏且不可交互、禁用 transform/transition/animation,并从该克隆元素读取宽高与边界矩形用于对齐/翻转/缩放计算;清理只移除 measureEl
对齐测量验证测试
tests/align.test.tsx
新增测试:渲染可见 Trigger 并触发依赖 resize 的重新对齐;通过临时覆盖 CSSStyleDeclaration 的 setter 记录 left/top/transform/transition/overflow 是否被修改,使用假时钟等待对齐完成并断言这些样式未被改动。

Sequence Diagram(s)

sequenceDiagram
    participant Trigger
    participant PopupParent as Popup 父容器
    participant Popup as Popup 元素
    participant MeasureEl as measureEl (克隆)
    participant Align as 对齐逻辑

    Trigger->>Align: 请求对齐(resize/重排)
    Align->>PopupParent: 创建 measureEl = Popup.cloneNode(false)
    PopupParent->>MeasureEl: append measureEl(隐藏、不可交互)
    MeasureEl->>Align: 返回 getBoundingClientRect()(宽/高/位置信息)
    Align->>Align: 计算位置/翻转/缩放
    Align->>Popup: 应用最终对齐(不修改测量相关样式)
    Align->>MeasureEl: remove()(清理克隆)
Loading

代码审查工作量评估

🎯 3 (中等) | ⏱️ ~20 分钟

可能相关的 PRs

推荐审阅人

  • afc163
  • yoyo837
  • zombieJ

诗歌

🐰 我分出一只小身影测量,悄悄躲在父容器旁,
既不碰也不移动,动画安然不受伤,
镜中尺寸轻声报,定位悄然回放,
胡萝卜跳舞庆祝,代码温柔又坦荡。

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题清晰准确地概括了主要变更:使用测量克隆以避免在对齐过程中破坏弹窗动画。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the popup alignment logic in useAlign.ts to prevent interference with CSS animations. It achieves this by using a temporary, cloned measurement element (measureEl) for calculations instead of directly modifying the popupElement's styles. This ensures that getBoundingClientRect() returns accurate values and avoids disrupting active CSS transitions or transforms on the popup. A new test case has been added to align.test.tsx to verify that the popup's styles (specifically transform, transition, and overflow) are no longer directly modified during the alignment measurement phase. The review suggests adding a try...finally block to prevent potential DOM leaks and enhancing the new test case to also assert that left and top styles are not modified on the popup element during measurement.

measureEl.style.transition = 'none';
measureEl.style.visibility = 'hidden';
measureEl.style.pointerEvents = 'none';
popupElement.parentElement?.appendChild(measureEl);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

To prevent DOM leaks if an error occurs during alignment, it's best practice to wrap the measurement logic that uses the temporary measureEl in a try...finally block, ensuring removeChild is always called.

For example:

      popupElement.parentElement?.appendChild(measureEl);
      let popupMirrorRect: DOMRect;

      try {
        // ... measurement logic ...
        popupMirrorRect = measureEl.getBoundingClientRect();
      } finally {
        popupElement.parentElement?.removeChild(measureEl);
      }

      // ... rest of the function ...

Note that popupMirrorRect would need to be declared as let outside the try block to be accessible later.

Comment on lines +379 to +381
expect(styleChanges).not.toContain('transform');
expect(styleChanges).not.toContain('transition');
expect(styleChanges).not.toContain('overflow');
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

To make this test more comprehensive and future-proof, consider also asserting that left and top styles are not modified during measurement. The previous implementation modified these properties, and ensuring they are not touched directly is a key part of this fix.

    expect(styleChanges).not.toContain('left');
    expect(styleChanges).not.toContain('top');
    expect(styleChanges).not.toContain('transform');
    expect(styleChanges).not.toContain('transition');
    expect(styleChanges).not.toContain('overflow');

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/hooks/useAlign.ts`:
- Around line 194-205: The shallow clone (popupElement.cloneNode(false))
produces an empty measureEl so getBoundingClientRect() returns 0×0 and breaks
the scale/alignment logic; fix by creating a measurement element that preserves
layout—either clone with children (cloneNode(true)) or immediately copy the
original element's computed size into measureEl (set style.width/style.height or
min-/max- styles from getComputedStyle or the original bounding rect) before
appending and measuring; ensure this change affects the same measurement block
(measureEl/popupElement -> popupWidth/popupHeight -> scaleX/scaleY) and apply
the same fix to the other clone block around lines 282-288 referenced in the
comment.

In `@tests/align.test.tsx`:
- Around line 363-381: The current test only spies on
popupElement.style.setProperty so direct assignments like style.transform =
'none' or style.left = 'auto' (seen in the implementation) evade detection;
update the test to spy on the CSSStyleDeclaration property setters used by the
implementation (e.g., spy on CSSStyleDeclaration.prototype for 'transform',
'transition', 'left', 'top', 'overflow' setters or wrap those property
descriptors) so assignments to style.transform/style.left/etc. are captured
instead of only setProperty.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c63dc6c9-4a54-40a3-9300-a632b72a0d5b

📥 Commits

Reviewing files that changed from the base of the PR and between 59b659d and 669642c.

📒 Files selected for processing (2)
  • src/hooks/useAlign.ts
  • tests/align.test.tsx

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/hooks/useAlign.ts`:
- Around line 205-207: The temporary measurement element (measureEl) can still
run CSS keyframe animations because cloneNode(false) preserves the
motionClassName; after disabling overflow/transform/transition in useAlign
(measureEl), set measureEl.style.animation = 'none' to stop keyframe animations
from affecting getBoundingClientRect(); update the code near where
measureEl.style.transition = 'none' is set and add a regression test using a
motion config with keyframes (e.g., rc-trigger-popup-zoom) to verify stable
positioning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bad5fa62-b289-4653-a7af-fc4882964c91

📥 Commits

Reviewing files that changed from the base of the PR and between d7767a0 and 3f8bcf6.

📒 Files selected for processing (1)
  • src/hooks/useAlign.ts

…g alignment

On some platforms (notably Linux with X11/Wayland compositors), the alignment
calculation runs mid-CSS-animation. The previous approach modified the popup
element's styles (left, top, transform, overflow) to measure its position,
which interfered with active CSS transitions (transition: all) and transforms
(transform: scale()) applied during entrance animations. This caused
getBoundingClientRect() to return incorrect values, producing wildly wrong
popup positions (e.g. top: -20000px).

The fix replaces the direct popup style manipulation with a shallow clone
(cloneNode(false)) used as a measurement proxy. The clone inherits the
popup's classes and attributes but has transform/transition/animation
explicitly neutralized. Dimensions are copied via offsetWidth/offsetHeight
(not getComputedStyle, which can return empty during the initial render
cycle). This allows accurate position measurement without touching the
original popup element, fully preserving CSS animations on all platforms.

Changes:
- Replace placeholder + popup style reset with cloneNode(false) measurement
- Copy offsetWidth/offsetHeight to clone for accurate dimensions
- Set transform/transition/animation to none on clone only
- Measure positions via the clone's getBoundingClientRect()
- Remove originLeft/Top/Right/Bottom/Overflow save/restore logic
@AnthonyH16 AnthonyH16 force-pushed the fix/neutralize-transform-during-align branch from 89469ce to 1543cec Compare March 25, 2026 01:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant