diff --git a/.changeset/hot-phones-sing.md b/.changeset/hot-phones-sing.md new file mode 100644 index 00000000000..83c4be66599 --- /dev/null +++ b/.changeset/hot-phones-sing.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +AnchoredOverlay: (Behind feature flag) Make popover API usage opt-in diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx index 2c8e149e7b1..345f379317e 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx @@ -21,6 +21,7 @@ type TestComponentSettings = { className?: string withCSSAnchorPositioningFeatureFlag?: boolean overlayProps?: Pick + renderAs?: 'portal' | 'popover' } const AnchoredOverlayTestComponent = ({ @@ -31,6 +32,7 @@ const AnchoredOverlayTestComponent = ({ className, withCSSAnchorPositioningFeatureFlag, overlayProps, + renderAs, }: TestComponentSettings = {}) => { const [open, setOpen] = useState(initiallyOpen) const onOpen = useCallback( @@ -57,6 +59,7 @@ const AnchoredOverlayTestComponent = ({ renderAnchor={props => } onPositionChange={onPositionChange} className={className} + renderAs={renderAs} {...overlayProps} > @@ -303,6 +306,52 @@ describe('AnchoredOverlay feature flag specific behavior', () => { const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') expect(overlay).toHaveAttribute('data-anchor-position', 'true') }) + + it('should set popover="manual" on overlay when renderAs is "popover"', () => { + const {baseElement} = render( + + + , + ) + + const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') + expect(overlay).toHaveAttribute('popover', 'manual') + }) + + it('should set popovertarget on anchor when renderAs is "popover"', () => { + const {baseElement} = render( + + + , + ) + + const anchor = baseElement.querySelector('[aria-haspopup="true"]') + const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') + expect(anchor).toHaveAttribute('popovertarget') + expect(anchor!.getAttribute('popovertarget')).toBe(overlay!.getAttribute('id')) + }) + + it('should not set popover attribute on overlay when renderAs is "portal"', () => { + const {baseElement} = render( + + + , + ) + + const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') + expect(overlay).not.toHaveAttribute('popover') + }) + + it('should not set popover attribute on overlay when renderAs defaults to "portal"', () => { + const {baseElement} = render( + + + , + ) + + const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') + expect(overlay).not.toHaveAttribute('popover') + }) }) describe('with primer_react_css_anchor_positioning feature flag disabled', () => { @@ -329,5 +378,16 @@ describe('AnchoredOverlay feature flag specific behavior', () => { const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') expect(overlay).toHaveAttribute('data-anchor-position', 'false') }) + + it('should not set popover attribute on overlay when renderAs is "popover" but flag is disabled', () => { + const {baseElement} = render( + + + , + ) + + const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') + expect(overlay).not.toHaveAttribute('popover') + }) }) }) diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx index 6fc65f1d2d2..31b4b755b8f 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx @@ -120,6 +120,11 @@ interface AnchoredOverlayBaseProps extends Pick + /** + * When `"popover"`, uses the Popover API only if the CSS anchor positioning feature flag is enabled + * and the browser supports native CSS anchor positioning. Has no effect otherwise. Defaults to `"portal"`. + */ + renderAs?: 'portal' | 'popover' } export type AnchoredOverlayProps = AnchoredOverlayBaseProps & @@ -162,11 +167,14 @@ export const AnchoredOverlay: React.FC { const cssAnchorPositioningFlag = useFeatureFlag('primer_react_css_anchor_positioning') const supportsNativeCSSAnchorPositioning = useRef(false) // eslint-disable-next-line react-hooks/refs const cssAnchorPositioning = cssAnchorPositioningFlag && supportsNativeCSSAnchorPositioning.current + // Only use Popover API when both CSS anchor positioning is enabled AND renderAs is true + const shouldRenderAsPopover = cssAnchorPositioning && renderAs === 'popover' const anchorRef = useProvidedRefOrCreate(externalAnchorRef) const [overlayRef, updateOverlayRef] = useRenderForcingRef() const anchorId = useId(externalAnchorId) @@ -286,15 +294,18 @@ export const AnchoredOverlay: React.FC { if (overlayProps?.ref) { assignRef(overlayProps.ref, node)