Skip to content

Codaxy/fix/iframe portal lookup dropdown position update - #1325

Open
milankovacevic-codaxy wants to merge 3 commits into
masterfrom
codaxy/fix/iframe-portal-lookup-dropdown-position-update
Open

Codaxy/fix/iframe portal lookup dropdown position update#1325
milankovacevic-codaxy wants to merge 3 commits into
masterfrom
codaxy/fix/iframe-portal-lookup-dropdown-position-update

Conversation

@milankovacevic-codaxy

Copy link
Copy Markdown
Contributor

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.

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