eric fix(#3540): add manual positioning to Popover#3655
eric fix(#3540): add manual positioning to Popover#3655willcodeforcoffee wants to merge 1 commit intodevfrom
Conversation
c4d4bde to
e2e7707
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds a JavaScript fallback to manually position goa-popover when CSS Anchor Positioning is not supported, so popovers don’t default to the page’s top-left in older browsers.
Changes:
- Added a manual positioning loop (RAF) to compute and apply
top/leftfor the popover when anchor positioning isn’t available. - Updated open/close handling to start/stop manual positioning and added extra close coordination logic for non-anchor browsers.
- Relaxed a few React browser specs by increasing
waitFortimeouts.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| libs/web-components/src/components/popover/Popover.svelte | Adds manual positioning logic and adjusts toggle/open/close behavior for non-anchor browsers. |
| libs/react-components/specs/popover.browser.spec.tsx | Increases waitFor timeouts for popover close/focus assertions. |
| libs/react-components/specs/app-header-menu.browser.spec.tsx | Increases waitFor timeout for app-header-menu close assertion. |
| const isAbove = | ||
| position === "above" || | ||
| (position === "auto" && _autoPosition === "above"); | ||
|
|
||
| if (isAbove) { | ||
| _popoverEl.style.top = `${targetRect.top - yOffset}px`; | ||
| _popoverEl.style.left = `${targetRect.left + xOffset}px`; | ||
| _popoverEl.style.transform = "translateY(-100%)"; | ||
| } else { | ||
| _popoverEl.style.top = `${targetRect.bottom + yOffset}px`; | ||
| _popoverEl.style.left = `${targetRect.left + xOffset}px`; | ||
| _popoverEl.style.transform = ""; | ||
| } |
There was a problem hiding this comment.
updatePopoverPosition only handles above/below/auto. When position === "right" and anchor positioning isn't supported, the popover will still be positioned below the target (and may retain position-right class styling that no longer makes sense). Manual positioning should include a right branch that matches the CSS-anchor behavior for position-right.
| function startManualPositioning() { | ||
| if (!_needsManualPositioning) return; | ||
|
|
||
| const loop = () => { | ||
| updatePopoverPosition(); | ||
| _positionRafId = requestAnimationFrame(loop); | ||
| }; | ||
| _positionRafId = requestAnimationFrame(loop); | ||
| } |
There was a problem hiding this comment.
startManualPositioning() can be called multiple times without checking/canceling an existing loop. If it gets invoked twice (e.g., duplicate toggle handling), the first RAF loop will keep running but its id is lost, causing a leak and extra per-frame work. Add a guard (return if _positionRafId is already set) or call stopManualPositioning() before starting a new loop.
| // Dispatch _open/_close events for consumer components | ||
| // (MenuButton, AppHeader, AppHeaderMenu, Dropdown) | ||
| if (_isOpen) { | ||
| dispatch(_rootEl, "_open", {}, { bubbles: true }); | ||
| dispatch(_rootEl, "_open"); | ||
| requestAnimationFrame(updateAutoPosition); // same vs await tick(), make sure popover element is fully rendered before we measure its dimension | ||
| startManualPositioning(); | ||
| } else { | ||
| _targetEl.focus(); | ||
| dispatch(_rootEl, "_close", {}, { bubbles: true }); | ||
| stopManualPositioning(); | ||
| _targetEl?.focus(); | ||
| dispatch(_rootEl, "_close"); | ||
| } |
There was a problem hiding this comment.
dispatch(_rootEl, "_open") / dispatch(_rootEl, "_close") now omit { bubbles: true }. The shared dispatch helper defaults bubbles to false when not provided, so this is a behavior change vs the previous bubbling events and may break consumers relying on event bubbling. Consider restoring { bubbles: true } (and any other previously set options) to avoid an accidental breaking change.
| await vi.waitFor( | ||
| () => { | ||
| const popoverContent = result.getByTestId("popover-content"); | ||
| expect(popoverContent.element().checkVisibility()).toBeFalsy(); | ||
| }, | ||
| { timeout: 10000 }, | ||
| ); |
There was a problem hiding this comment.
This waitFor was bumped to a 10s timeout, which is notably higher than other tests in this suite (commonly 1–3s). If possible, prefer keeping timeouts small/consistent and fix the underlying close timing issue so the test doesn't need such a large budget.
| await vi.waitFor( | |
| () => { | |
| const popoverContent = result.getByTestId("popover-content"); | |
| expect(popoverContent.element().checkVisibility()).toBeFalsy(); | |
| }, | |
| { timeout: 10000 }, | |
| ); | |
| await vi.waitFor(() => { | |
| const popoverContent = result.getByTestId("popover-content"); | |
| expect(popoverContent.element().checkVisibility()).toBeFalsy(); | |
| }); |
| if (isAbove) { | ||
| _popoverEl.style.top = `${targetRect.top - yOffset}px`; | ||
| _popoverEl.style.left = `${targetRect.left + xOffset}px`; | ||
| _popoverEl.style.transform = "translateY(-100%)"; | ||
| } else { | ||
| _popoverEl.style.top = `${targetRect.bottom + yOffset}px`; | ||
| _popoverEl.style.left = `${targetRect.left + xOffset}px`; | ||
| _popoverEl.style.transform = ""; | ||
| } |
There was a problem hiding this comment.
Manual positioning sets top/left and also applies transform, but .popover-content still has the CSS translate: rules (including the position-above -100% shift). In browsers without anchor positioning support this will compound transforms/offsets (e.g., position-above ends up translating by -200% and offsets are effectively applied twice), producing incorrect placement. When _needsManualPositioning is true, disable/override the CSS translate behavior (or rely on it exclusively) so the JS positioning math isn't compounded by existing transform rules.
|
|
||
| // Functions | ||
|
|
||
| function updatePopoverPosition() { |
There was a problem hiding this comment.
Safari 18 will ignore Anchor Positioning, but it won't ignore all the translate calls I put in here. So when you're manually calculating the positioning here, the translate calls are still running. This means when someone is using below, the horizontal and vertical offset will be doubled. And when someone is using above, the position of the popover will be twice its height above the element.
…ot support CSS anchor positioning
7084f2e to
cae35f7
Compare






This PR adds manual positioning for a Popover when the browser does not support CSS anchor positioning.
Before (the change)
The popover would work only on browsers that support Anchor Positioning. On browsers that do not support Anchor Positioning all popovers would appear in the top left corner of a page.
After (the change)
The popover works on older browsers that do not support Anchor Positioning.
Make sure that you've checked the boxes below before you submit the PR
Steps needed to test