Fix dropdown positioning when a related element lives inside an iframe - clean version - #1324
Closed
milankovacevic-codaxy wants to merge 2 commits into
Closed
Conversation
… col) Pure formatting change, no logic. Brings Dropdown.tsx in line with the rest of the codebase; separated out so the upcoming iframe positioning fix can land as a small, reviewable diff. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
milankovacevic-codaxy
deleted the
codaxy/fix/iframe-portal-lookup-dropdown-position-clean
branch
September 4, 2026 12:54
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
LookupField (and other Dropdown-based fields: ColorField, DateTimeField, MonthField) mispositions its popup and fails to dismiss correctly when the field is rendered inside an <iframe> via a React portal (single React tree / JS realm, DOM output split across the top document and the iframe's own document; the same technique used by CSS-isolation helpers like react-frame-component).
Two independent bugs combine to produce this:
Popup renders in the wrong coordinate space. On a normal desktop pointer, Dropdown's popup is rendered inline (not portaled to document.body). When the field lives inside an iframe, that popup ends up as a DOM child inside the iframe's own document, so its position: fixed resolves against the iframe's viewport. But Dropdown.updateDropdownPosition always called getTopLevelBoundingClientRect(relatedElement), which unconditionally adds the iframe's own offset within the top document - a conversion that's only correct when the popup is actually portaled into the top document (e.g. Window, Tooltip, touch-friendly dropdowns). The result: the popup renders shifted by roughly the iframe's own left/top offset instead of appearing next to the field.
Dismiss-on-focus-out can't see across the iframe boundary. getActiveElement() read only the top document's document.activeElement, which the browser reports as the <iframe> element itself for any focus change inside it and never the actual focused element, and unchanged for every subsequent focus move within that iframe. FocusManager's polling loop and isSelfOrDescendant (el.contains(...), which is always false across documents) then misfire: opening the dropdown triggers an immediate false "focus left" dismissal (visible as an open/close flicker), and afterwards no further focus changes inside the iframe are ever detected, so the dropdown won't dismiss until focus returns to the top document.
Root cause:
Several places in the positioning/focus code implicitly assumed a single, global document - correct for the common case, but wrong once part of the widget tree renders inside a different document that shares the same JS realm.
Solution:
packages/cx/src/widgets/overlay/Dropdown.tsx
updateDropdownPosition: only convert relatedElement's rect into top-document coordinates when the popup element (el) and relatedElement actually live in different documents; otherwise use relatedElement.getBoundingClientRect() directly (they already share a coordinate space).
applyFixedPositioningPlacementStyles / applyAbsolutePositioningPlacementStyles: derive viewport width/height from el.ownerDocument instead of the global document, so available-room/flip-placement math and edge-anchored (right/bottom) styles are correct when the popup's containing viewport isn't the top document.
findOptimalPlacement: now takes the popup element so its placement scoring uses the same document-correct viewport.
getViewportRect (module helper): takes an optional doc parameter, defaulting to the global document - no behavior change for the non-iframe case.
packages/cx/src/util/getActiveElement.ts
getActiveElement() now recurses into a focused <iframe>'s own contentDocument (recursively, for nested iframes) to return the truly-focused element, instead of stopping at the <iframe> node. This is a shared low-level fix - every consumer (FocusManager, Overlay's focus-out handling, blur checks in ColorField/DateTimeField/MonthField/NumberField/TextArea/TextField/Grid/MenuItem) benefits with no call-site changes.
Both fixes are additive/conditional: for the standard (non-iframe) case, el.ownerDocument === relatedElement.ownerDocument and doc.activeElement is never an <iframe>, so the new code paths are no-ops and existing behavior is unchanged.
Test plan
Added litmus repros under litmus/features/dropdown/ for manual verification (and for comparing against a genuinely separate iframe document, where this bug doesn't apply):
lookup-inside-iframe-portal.js + IFramePortal.js - reproduces both bugs (portal-based iframe embedding).
lookup-inside-real-iframe.js - control case: a LookupField in a truly separate iframe document/window, for comparison.
Manual checks:
Open the dropdown for the iframe-portal LookupField - it opens directly next to the field (no offset), matching the non-iframe field on the same page.
No open/close flicker on first click.
Clicking other content inside the same iframe dismisses the dropdown.
Clicking outside the iframe still dismisses the dropdown (regression check).
Existing non-iframe dropdown/overlay behavior (Window, Tooltip, context menus, LookupField/ColorField/DateTimeField/MonthField) is unaffected.