Skip to content

eric fix(#3540): add manual positioning to Popover#3655

Open
willcodeforcoffee wants to merge 1 commit intodevfrom
eric/3540-manual-anchor-positioning
Open

eric fix(#3540): add manual positioning to Popover#3655
willcodeforcoffee wants to merge 1 commit intodevfrom
eric/3540-manual-anchor-positioning

Conversation

@willcodeforcoffee
Copy link
Collaborator

@willcodeforcoffee willcodeforcoffee commented Mar 24, 2026

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.

image

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

  • I have read and followed the setup steps
  • I have created necessary unit tests
  • I have tested the functionality in both React and Angular.

Steps needed to test

@willcodeforcoffee willcodeforcoffee self-assigned this Mar 24, 2026
@willcodeforcoffee willcodeforcoffee added Popover P1 Priority 1 (must have): Prevents product teams from adopting/using DS 2.0 as intended at launch. labels Mar 24, 2026
@willcodeforcoffee willcodeforcoffee linked an issue Mar 24, 2026 that may be closed by this pull request
@willcodeforcoffee willcodeforcoffee force-pushed the eric/3540-manual-anchor-positioning branch 2 times, most recently from c4d4bde to e2e7707 Compare March 24, 2026 17:31
@willcodeforcoffee willcodeforcoffee marked this pull request as ready for review March 24, 2026 17:48
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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/left for 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 waitFor timeouts.

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.

Comment on lines +156 to +168
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 = "";
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +171 to +179
function startManualPositioning() {
if (!_needsManualPositioning) return;

const loop = () => {
updatePopoverPosition();
_positionRafId = requestAnimationFrame(loop);
};
_positionRafId = requestAnimationFrame(loop);
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines 258 to 268
// 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");
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +62
await vi.waitFor(
() => {
const popoverContent = result.getByTestId("popover-content");
expect(popoverContent.element().checkVisibility()).toBeFalsy();
},
{ timeout: 10000 },
);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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();
});

Copilot uses AI. Check for mistakes.
Comment on lines +160 to +168
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 = "";
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

@bdfranck bdfranck left a comment

Choose a reason for hiding this comment

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

I looked at the "Feature #3478: Popover API Rewrite" test scenarios...

  • ✅ The scenarios work with Safari 18
Image Image Image
  • ✅ The scenarios still work with the latest version of Safari
Image Image Image

Looks good to me! 👍


// Functions

function updatePopoverPosition() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

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.

@willcodeforcoffee willcodeforcoffee force-pushed the eric/3540-manual-anchor-positioning branch from 7084f2e to cae35f7 Compare March 24, 2026 23:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P1 Priority 1 (must have): Prevents product teams from adopting/using DS 2.0 as intended at launch. Popover

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Popover: Safari 18 fallback for CSS anchor positioning

4 participants