[CSS Anchor Positioning] Ensure AnchoredOverlay remains in the viewport#7802
[CSS Anchor Positioning] Ensure AnchoredOverlay remains in the viewport#7802
AnchoredOverlay remains in the viewport#7802Conversation
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
🦋 Changeset detectedLatest commit: 00afccf The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
AnchoredOverlay remains in the viewport
| // Suggest a flip when the overlay is currently overflowing both axes: | ||
| // prefer the side of the anchor with enough room, otherwise fall back to | ||
| // outside-bottom and let the offsets keep it inside the viewport. |
There was a problem hiding this comment.
The summary of the changes here are:
- We try to account for content overflow both horizontally and vertically. CSS anchor positioning will handle this automatically IF there's at least one fallback with enough space to fit the overlay, but if not, it'll default the the default position, which is where this implementation comes in.
- This should only kick in only when CSS anchor positioning isn't able to correctly position the overlay (lack of space).
- This logic only runs on opening of the overlay, so it won't affect performance.
| // Manage `anchor-name` on the anchor independently of `open`/`width` so a | ||
| // parent re-render that re-runs the positioning effect below doesn't | ||
| // briefly flicker the anchor link off and back on. |
There was a problem hiding this comment.
The comment explains it, but we add an additional useEffect mainly to handle when we add/remove the anchor-overlay CSS anchor positioning link.
It helps us ensure that unrelated dependency changes don't accidentally add or remove position-anchor or anchor-name.
| /* Falls back to `anchor(top)` when JS hasn't overridden it. JS sets the | ||
| override when the overlay's bottom would overflow the viewport so we | ||
| can lift it up to keep the bottom edge on screen, mirroring the JS | ||
| anchored-positioning code path. */ | ||
| top: var(--anchored-overlay-top-override, anchor(top)); |
There was a problem hiding this comment.
This is some new logic to handle overflows in the y-axis. Mainly to fix https://github.com/github/primer/issues/6648 gracefully.
|
👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/19900 |
There was a problem hiding this comment.
Pull request overview
This PR updates the CSS-anchor-positioning path for AnchoredOverlay so overlays and nested ActionMenu submenus stay within the viewport more reliably when there is limited horizontal and vertical space. It fits into the existing feature-flagged native anchor-positioning rollout in @primer/react.
Changes:
- Split anchor-name management from the positioning effect and added new JS fallback logic for side suggestions and viewport clamping in
AnchoredOverlay. - Added CSS support for a JS-controlled top override on left/right anchored overlays.
- Added a new ActionMenu dev story to manually exercise large right-aligned submenus, plus a patch changeset entry.
Show a summary per file
| File | Description |
|---|---|
packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx |
Adds native anchor-positioning fallback logic for horizontal alignment, suggested side changes, and vertical clamping. |
packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css |
Wires left/right anchored overlays to a JS-provided top override custom property. |
packages/react/src/ActionMenu/ActionMenu.dev.stories.tsx |
Adds a dev-only preview scenario for large nested submenus near the viewport edge. |
.changeset/social-deer-laugh.md |
Records the patch release note for the viewport-fitting behavior change. |
Copilot's findings
Comments suppressed due to low confidence (2)
packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx:318
- This only corrects bottom overflow. If a left/right overlay gets flipped above its anchor and ends up with a negative
topbut no bottom overflow, we now leave it partially off-screen at the top of the viewport. The new viewport-clamping logic needs to handle top overflow as well.
// Set y-axis offset to prevent overflow if needed.
const settledRect = currentOverlay.getBoundingClientRect()
const overflowBottom = settledRect.bottom - window.innerHeight
if (overflowBottom > 0) {
const clampedTop = Math.max(0, settledRect.top - overflowBottom - 8)
currentOverlay.style.setProperty('--anchored-overlay-top-override', `${clampedTop}px`)
} else {
currentOverlay.style.removeProperty('--anchored-overlay-top-override')
packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx:305
- If a previous measurement set
data-sideto a suggested fallback, a later run that no longer needs a flip never restores the requestedside. Because this is a direct DOM mutation, React will not reconcile it back to the prop value on a rerender with the same props, so the overlay can stay stuck on the old side after width/content changes while it is open.
if (result.suggestedSide) {
currentOverlay.setAttribute('data-side', result.suggestedSide)
- Files reviewed: 4/4 changed files
- Comments generated: 3
| const fallbackWidth = width ? parseInt(widthMap[width]) : parseInt(widthMap.small) | ||
| const result = getDefaultPosition(anchorElement, currentOverlay, fallbackWidth) | ||
|
|
||
| currentOverlay.setAttribute('data-align', result.horizontal) | ||
| if (result.suggestedSide) { | ||
| currentOverlay.setAttribute('data-side', result.suggestedSide) | ||
| } | ||
|
|
||
| // Apply offset only when viewport is too narrow | ||
| const offset = result.horizontal === 'left' ? result.leftOffset : result.rightOffset | ||
| currentOverlay.style.setProperty(`--anchored-overlay-anchor-offset-${result.horizontal}`, `${offset || 0}px`) | ||
|
|
||
| // Set y-axis offset to prevent overflow if needed. | ||
| const settledRect = currentOverlay.getBoundingClientRect() | ||
| const overflowBottom = settledRect.bottom - window.innerHeight | ||
| if (overflowBottom > 0) { | ||
| const clampedTop = Math.max(0, settledRect.top - overflowBottom - 8) | ||
| currentOverlay.style.setProperty('--anchored-overlay-top-override', `${clampedTop}px`) | ||
| } else { | ||
| currentOverlay.style.removeProperty('--anchored-overlay-top-override') |
AnchoredOverlay remains in the viewportAnchoredOverlay remains in the viewport
Closes https://github.com/github/primer/issues/6511
Adds behavior to handle overflow in y-axis when CSS anchor positioning does not have enough space to reposition. We handled this before in #7725, but this only applied to the x-axis (left | right).
This PR also implements behavior to ensure that the overlay fits in the viewport if its height expands past the viewport height.
Changelog
Changed
Rollout strategy
Testing & Reviewing
Merge checklist