Codaxy/fix/iframe portal lookup dropdown position update - #1325
Conversation
ReviewNine findings. Correctness issues first, then quality. Correctness1. Iframe-aware Drilling into the focused iframe's document returns an inner element, but Fix: make containment frame-aware (walk 2. Dropdown inside an iframe never repositions on the iframe's own scroll or resize — The new same-document branch positions Fix: push 3. Viewport size for an iframe uses the
Example: style-isolated iframe 600px tall, content ~150px. Fix: use 4.
Fix: 5. The same-document branch produces iframe-local Fix: subtract 6. Two disagreeing definitions of "the focused element" —
Fix: make the DOM.ts helpers delegate to Quality7. Litmus labels are swapped and the prose is wrong — The field labelled "IFrame Portal Lookup (inline)" is the one configured with 8. Owning document resolved three different ways —
9. Whole-file reindent mixed into the fix commit — Commit 9c5ed96 reindents the file from 2-space to the .editorconfig 3-space style (raw diff 895+/884-, whitespace-insensitive 31+/20-) in the same commit as the logic fix. The GitHub diff shows every line changed so the ~50-line fix is unreviewable without |
- Implemented context menu handling for focusable elements within iframes to prevent premature dismissal. - Updated dropdown positioning logic to account for elements residing in different documents (iframes). - Improved active element retrieval across document boundaries to ensure accurate focus management.
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.