Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7d19757
Replace polyfill with `useAnchoredPosition`
TylerJDev Apr 2, 2026
2515e42
Adjust CSS
TylerJDev Apr 2, 2026
7dee64b
Add some JS
TylerJDev Apr 6, 2026
8125f08
Try more lightweight solution
TylerJDev Apr 6, 2026
1841ecc
Improve logic
TylerJDev Apr 6, 2026
848c19c
Add story to `dev`
TylerJDev Apr 6, 2026
cc8c862
chore: auto-fix lint and formatting issues
TylerJDev Apr 6, 2026
8a5fedf
Remove `max-height`
TylerJDev Apr 9, 2026
1162d6c
Remove `max-height` from `popopover`
TylerJDev Apr 9, 2026
077bf38
Add better offset
TylerJDev Apr 9, 2026
87c58e8
Add more logic for repositioning
TylerJDev Apr 9, 2026
cb72ef8
chore: auto-fix lint and formatting issues
TylerJDev Apr 9, 2026
f49729c
test(vrt): update snapshots
TylerJDev Apr 10, 2026
bdaee56
Fix CSS
TylerJDev Apr 10, 2026
31369df
Remove stories
TylerJDev Apr 10, 2026
1388a51
Add changeset
TylerJDev Apr 10, 2026
463f087
Remove snapshots
TylerJDev Apr 10, 2026
e927b42
Remove one more
TylerJDev Apr 10, 2026
5a0e340
test(vrt): update snapshots
TylerJDev Apr 10, 2026
e7989c0
Adjust function to still apply `data-align`
TylerJDev Apr 13, 2026
c88c994
Update based on size width, not viewport
TylerJDev Apr 13, 2026
621ccab
test(vrt): update snapshots
TylerJDev Apr 13, 2026
6d98342
Address feedback
TylerJDev Apr 14, 2026
9763d3b
chore: auto-fix lint and formatting issues
TylerJDev Apr 14, 2026
5e712d0
Update packages/react/src/Overlay/Overlay.module.css
TylerJDev Apr 14, 2026
b92d59f
Update .changeset/eight-eagles-deny.md
TylerJDev Apr 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eight-eagles-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

AnchoredOverlay: Remove polyfill for CSS Anchor Positioning, use primer/behaviors as fallback. Ensure overlays take available space.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Odd?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm thinking that the added is the correct version. We used to override max-height, which meant that content would just expand the full height of the content. Now we respect the max-height set.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

are there similar instances in github-ui that we need to verify?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah! This issue is what made me make the change: https://github.com/github/primer/issues/6538. It uses max-height

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@
"@github/relative-time-element": "^4.5.0",
"@github/tab-container-element": "^4.8.2",
"@lit-labs/react": "1.2.1",
"@oddbird/css-anchor-positioning": "^0.9.0",
"@oddbird/popover-polyfill": "^0.5.2",
"@primer/behaviors": "^1.10.2",
"@primer/live-region-element": "^0.7.1",
Expand Down
27 changes: 26 additions & 1 deletion packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,31 +27,56 @@
margin: 0;
padding: 0;
border: 0;
max-height: none;
max-width: none;
}

&[data-side='outside-bottom'] {
/* stylelint-disable primer/spacing */
top: calc(anchor(bottom) + var(--base-size-4));
left: anchor(left);

&[data-align='left'] {
left: auto;
right: calc(anchor(right) - var(--anchored-overlay-anchor-offset-left));
}
}

&[data-side='outside-top'] {
margin-bottom: var(--base-size-4);
bottom: anchor(top);
left: anchor(left);

&[data-align='left'] {
left: auto;
right: anchor(right);
}
}

&[data-side='outside-left'] {
right: anchor(left);
top: anchor(top);
margin-right: var(--base-size-4);
position-try-fallbacks: flip-inline, flip-block, flip-start, --outside-left-to-bottom;
}

&[data-side='outside-right'] {
left: anchor(right);
top: anchor(top);
margin-left: var(--base-size-4);
position-try-fallbacks: flip-inline, flip-block, flip-start, --outside-right-to-bottom;
}
}

@position-try --outside-left-to-bottom {
right: anchor(right);
top: calc(anchor(bottom) + var(--base-size-4));
margin: 0;
width: auto;
}

@position-try --outside-right-to-bottom {
left: anchor(left);
top: calc(anchor(bottom) + var(--base-size-4));
margin: 0;
width: auto;
}
Comment on lines +70 to +82
Copy link
Copy Markdown
Member Author

@TylerJDev TylerJDev Apr 9, 2026

Choose a reason for hiding this comment

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

Adding some more fallbacks for side='outside-right' and side='outside-left'. Added stories for both too.

67 changes: 46 additions & 21 deletions packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {XIcon} from '@primer/octicons-react'
import classes from './AnchoredOverlay.module.css'
import {clsx} from 'clsx'
import {useFeatureFlag} from '../FeatureFlags'
import {widthMap} from '../Overlay/Overlay'

interface AnchoredOverlayPropsWithAnchor {
/**
Expand Down Expand Up @@ -125,17 +126,6 @@ export type AnchoredOverlayProps = AnchoredOverlayBaseProps &
(AnchoredOverlayPropsWithAnchor | AnchoredOverlayPropsWithoutAnchor) &
Partial<Pick<PositionSettings, 'align' | 'side' | 'anchorOffset' | 'alignmentOffset' | 'displayInViewport'>>

const applyAnchorPositioningPolyfill = async () => {
if (typeof window !== 'undefined' && !('anchorName' in document.documentElement.style)) {
try {
await import('@oddbird/css-anchor-positioning')
} catch (e) {
// eslint-disable-next-line no-console
console.warn('Failed to load CSS anchor positioning polyfill:', e)
}
}
}

const defaultVariant = {
regular: 'anchored',
narrow: 'anchored',
Expand Down Expand Up @@ -173,7 +163,9 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
displayCloseButton = true,
closeButtonProps = defaultCloseButtonProps,
}) => {
const cssAnchorPositioning = useFeatureFlag('primer_react_css_anchor_positioning')
const cssAnchorPositioningFlag = useFeatureFlag('primer_react_css_anchor_positioning')
const supportsNativeCSSAnchorPositioning = useRef(false)
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

cssAnchorPositioning is derived from supportsNativeCSSAnchorPositioning.current, but that ref is only set in a useEffect. Updating a ref does not trigger a re-render, so cssAnchorPositioning will stay false for the lifetime of the component (meaning the CSS anchor positioning path never activates even on supporting browsers when the feature flag is enabled). Consider computing support synchronously during render with a typeof document !== 'undefined' guard, or store support in state and update it in an effect.

Suggested change
const supportsNativeCSSAnchorPositioning = useRef(false)
const supportsNativeCSSAnchorPositioning = useRef(
typeof document !== 'undefined' &&
typeof window !== 'undefined' &&
typeof window.CSS !== 'undefined' &&
window.CSS.supports('anchor-name: --primer-overlay-anchor') &&
window.CSS.supports('position-anchor: --primer-overlay-anchor'),
)

Copilot uses AI. Check for mistakes.
const cssAnchorPositioning = cssAnchorPositioningFlag && supportsNativeCSSAnchorPositioning.current
const anchorRef = useProvidedRefOrCreate(externalAnchorRef)
const [overlayRef, updateOverlayRef] = useRenderForcingRef<HTMLDivElement>()
const anchorId = useId(externalAnchorId)
Expand Down Expand Up @@ -232,19 +224,14 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
[overlayRef.current],
)

const hasLoadedAnchorPositioningPolyfill = useRef(false)

useEffect(() => {
supportsNativeCSSAnchorPositioning.current = 'anchorName' in document.documentElement.style

// ensure overlay ref gets cleared when closed, so position can reset between closing/re-opening
if (!open && overlayRef.current) {
updateOverlayRef(null)
}

if (cssAnchorPositioning && !hasLoadedAnchorPositioningPolyfill.current) {
applyAnchorPositioningPolyfill()
hasLoadedAnchorPositioningPolyfill.current = true
}
}, [open, overlayRef, updateOverlayRef, cssAnchorPositioning])
}, [open, overlayRef, updateOverlayRef])

useFocusZone({
containerRef: overlayRef,
Expand Down Expand Up @@ -282,14 +269,27 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr

if (!cssAnchorPositioning || !open || !currentOverlay) return
currentOverlay.style.setProperty('position-anchor', `--anchored-overlay-anchor-${id}`)

const anchorElement = anchorRef.current
if (anchorElement) {
const overlayWidth = width ? parseInt(widthMap[width]) : null
const result = getDefaultPosition(anchorElement, overlayWidth)

currentOverlay.setAttribute('data-align', result.horizontal)

// 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`)
}

try {
if (!currentOverlay.matches(':popover-open')) {
currentOverlay.showPopover()
}
} catch {
// Ignore if popover is already showing or not supported
}
}, [cssAnchorPositioning, open, overlayElement, id, overlayRef])
}, [cssAnchorPositioning, open, overlayElement, id, overlayRef, anchorRef, width])

const showXIcon = onClose && variant.narrow === 'fullscreen' && displayCloseButton
const XButtonAriaLabelledBy = closeButtonProps['aria-labelledby']
Expand Down Expand Up @@ -365,6 +365,31 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
)
}

function getDefaultPosition(
anchorElement: HTMLElement,
overlayWidth: number | null,
): {horizontal: 'left' | 'right'; leftOffset?: number; rightOffset?: number} {
const rect = anchorElement.getBoundingClientRect()
const vw = window.innerWidth
const viewportMargin = 8
const spaceLeft = rect.left
const spaceRight = vw - rect.right
const horizontal: 'left' | 'right' = spaceLeft > spaceRight ? 'left' : 'right'

// If there's no explicit overlay width, or either side has enough space
// to contain the overlay, let CSS position-try-fallbacks handle positioning
if (!overlayWidth || spaceLeft >= overlayWidth + viewportMargin || spaceRight >= overlayWidth + viewportMargin) {
return {horizontal}
}

// If the viewport is too narrow to fit the overlay on either side, calculate offsets to prevent overflow
// leftOffset is how much to shift the overlay to the right, rightOffset is how much to shift the overlay to the left
const leftOffset = Math.max(0, overlayWidth - rect.right + viewportMargin)
const rightOffset = Math.max(0, rect.left + overlayWidth - vw + viewportMargin)
Comment on lines +387 to +388
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I took a second to understand these 😅

Is leftOffset from ensuring overlayLeft >= viewportMargin, so viewportMargin - (rect.right - overlayWidth)>=0

and rightOffset from overlayRight <= vw - viewportMargin?

Could we add a short comment here explaining the formulas?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You're right, it can be a bit confusing 😅 I added a comment to clarify! Thanks for the suggestion too!


return {horizontal, leftOffset, rightOffset}
}
Comment on lines +368 to +391
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We calculate the amount of space available surrounding the anchor (x-axis). If there's not enough space for the overlay in the viewport (e.g., viewport width is 600px while the overlay width is 640px), we'll add an offset.


function assignRef<T>(
ref: React.MutableRefObject<T | null> | ((instance: T | null) => void) | null | undefined,
value: T | null,
Expand Down
3 changes: 2 additions & 1 deletion packages/react/src/Overlay/Overlay.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@
visibility: hidden;
}

&:where([data-responsive='fullscreen']) {
&:where([data-responsive='fullscreen']),
&[data-responsive='fullscreen'][data-anchor-position='true'] {
@media screen and (--viewportRange-narrow) {
position: fixed;
top: 0;
Expand Down
3 changes: 1 addition & 2 deletions packages/react/src/Overlay/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ export const heightMap = {
'fit-content': 'fit-content',
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-useless-assignment
const widthMap = {
export const widthMap = {
small: '256px',
medium: '320px',
large: '480px',
Expand Down
Loading