diff --git a/packages/react/package.json b/packages/react/package.json index d4ae30e6638..21c1ee30aa9 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -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", diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.dev.stories.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.dev.stories.tsx index 88f7a778309..a6746108422 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.dev.stories.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.dev.stories.tsx @@ -4,7 +4,7 @@ import React, {useState, useRef} from 'react' import {Button} from '../Button' import {AnchoredOverlay} from '.' import {Stack} from '../Stack' -import {Dialog, Spinner, ActionList, ActionMenu} from '..' +import {Dialog, Spinner, ActionList, ActionMenu, Text} from '..' const meta = { title: 'Components/AnchoredOverlay/Dev', @@ -309,3 +309,50 @@ export const WithActionMenu = { }, }, } + +export const SmallViewportRightAligned = { + render: () => { + const [open, setOpen] = useState(false) + + return ( +
+ setOpen(true)} + onClose={() => setOpen(false)} + renderAnchor={props => } + overlayProps={{ + role: 'dialog', + 'aria-modal': true, + 'aria-label': 'Small viewport positioning test', + style: {minWidth: '320px'}, + }} + width="xlarge" + focusZoneSettings={{disabled: true}} + preventOverflow={false} + > +
+ + Overlay content + + This overlay is wider than the available space to the left of the anchor. It should reposition to avoid + overflowing the viewport. + + +
+
+
+ ) + }, + parameters: { + viewport: { + defaultViewport: 'small', + }, + docs: { + description: { + story: + 'Tests overlay positioning when the trigger button is right-aligned on a small viewport. The overlay is wider than the space to the left of the anchor.', + }, + }, + }, +} diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css b/packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css index 5edbdb0a043..f9b98ee3d5e 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css @@ -19,6 +19,7 @@ flip-inline, flip-block flip-inline; position-visibility: anchors-visible; + position-try-order: most-inline-size; z-index: 100; position: fixed !important; @@ -35,23 +36,37 @@ /* stylelint-disable primer/spacing */ top: calc(anchor(bottom) + var(--base-size-4)); left: anchor(left); + max-height: calc(100dvh - anchor(bottom) - var(--base-size-4)); + + &[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); + max-height: calc(anchor(top) - var(--base-size-4)); + + &[data-align='left'] { + left: auto; + right: anchor(right); + } } &[data-side='outside-left'] { right: anchor(left); top: anchor(top); margin-right: var(--base-size-4); + max-height: calc(100dvh - anchor(top)); } &[data-side='outside-right'] { left: anchor(right); top: anchor(top); margin-left: var(--base-size-4); + max-height: calc(100dvh - anchor(top)); } } diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx index 65af71b83dd..48fda14a9a6 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx @@ -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 { /** @@ -125,17 +126,6 @@ export type AnchoredOverlayProps = AnchoredOverlayBaseProps & (AnchoredOverlayPropsWithAnchor | AnchoredOverlayPropsWithoutAnchor) & Partial> -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', @@ -173,7 +163,9 @@ export const AnchoredOverlay: React.FC { - const cssAnchorPositioning = useFeatureFlag('primer_react_css_anchor_positioning') + const cssAnchorPositioningFlag = useFeatureFlag('primer_react_css_anchor_positioning') + const supportsNativeCSSAnchorPositioning = useRef(false) + const cssAnchorPositioning = cssAnchorPositioningFlag && supportsNativeCSSAnchorPositioning.current const anchorRef = useProvidedRefOrCreate(externalAnchorRef) const [overlayRef, updateOverlayRef] = useRenderForcingRef() const anchorId = useId(externalAnchorId) @@ -232,19 +224,14 @@ export const AnchoredOverlay: React.FC { + 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, @@ -282,6 +269,18 @@ export const AnchoredOverlay: React.FC= overlayWidth) return null + + const horizontal = vw - rect.right >= rect.left ? 'right' : 'left' + const offset = overlayWidth - rect.left + + return {horizontal, offset} +} + function assignRef( ref: React.MutableRefObject | ((instance: T | null) => void) | null | undefined, value: T | null, diff --git a/packages/react/src/Overlay/Overlay.tsx b/packages/react/src/Overlay/Overlay.tsx index f495d015730..9dcd41da021 100644 --- a/packages/react/src/Overlay/Overlay.tsx +++ b/packages/react/src/Overlay/Overlay.tsx @@ -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',