@@ -36,6 +36,7 @@ export interface PopoverPositionStyle {
3636 alignSelf ?: string ;
3737 marginInlineStart ?: string ;
3838 marginBlockStart ?: string ;
39+ translate ?: string ;
3940 top ?: string ;
4041 bottom ?: string ;
4142 left ?: string ;
@@ -71,6 +72,33 @@ function getCrossAxisAvailable(
7172 return Math . min ( center - boundaryStart , boundaryEnd - center ) * 2 ;
7273}
7374
75+ function shiftCrossAxis ( value : number , boundaryStart : number , boundaryEnd : number , size : number ) : number {
76+ const max = boundaryEnd - size ;
77+ return max < boundaryStart ? boundaryStart : clamp ( value , boundaryStart , max ) ;
78+ }
79+
80+ function getAnchorCrossAxisShift (
81+ start : number ,
82+ end : number ,
83+ size : number ,
84+ boundaryStart : number ,
85+ boundaryEnd : number ,
86+ align : PopoverAlign ,
87+ alignOffset : number ,
88+ boundaryOffset : number
89+ ) : { base : string ; translate : string } {
90+ const base =
91+ align === 'start' ? start + alignOffset : align === 'end' ? end + alignOffset : start + size / 2 + alignOffset ;
92+ const desiredTranslate = align === 'start' ? '0px' : align === 'end' ? '-100%' : '-50%' ;
93+
94+ return {
95+ base : `${ base } px` ,
96+ translate : `clamp(${ boundaryStart + boundaryOffset - base } px, ${ desiredTranslate } , calc(${
97+ boundaryEnd - boundaryOffset - base
98+ } px - 100%))`,
99+ } ;
100+ }
101+
74102/**
75103 * Get positioning styles for the popup element.
76104 *
@@ -97,7 +125,7 @@ export function getAnchorPositionStyle(
97125) : PopoverPositionStyle & Record < string , string | undefined > {
98126 if ( supportsAnchorPositioning ( ) ) {
99127 return {
100- ...getAnchorPositionCSS ( anchorName , opts , cssVars ) ,
128+ ...getAnchorPositionCSS ( anchorName , opts , cssVars , triggerRect , boundaryRect , offsets ) ,
101129 ...( triggerRect && boundaryRect ? getPositioningCSSVars ( triggerRect , boundaryRect , opts , offsets , cssVars ) : { } ) ,
102130 } ;
103131 }
@@ -106,7 +134,7 @@ export function getAnchorPositionStyle(
106134 if ( triggerRect && popupRect ) {
107135 const resolved : ManualOffsets = offsets ?? ZERO_OFFSETS ;
108136 return {
109- ...getManualPositionStyle ( triggerRect , popupRect , opts , resolved ) ,
137+ ...getManualPositionStyle ( triggerRect , popupRect , opts , resolved , boundaryRect ) ,
110138 ...( boundaryRect ? getPositioningCSSVars ( triggerRect , boundaryRect , opts , resolved , cssVars ) : { } ) ,
111139 position : 'fixed' ,
112140 // Reset UA [popover] defaults (inset: 0; margin: auto) which would
@@ -128,11 +156,15 @@ export function getAnchorNameStyle(anchorName: string) {
128156function getAnchorPositionCSS (
129157 anchorName : string ,
130158 opts : PositioningOptions ,
131- cssVars : PositioningCSSVars = PopoverCSSVars
159+ cssVars : PositioningCSSVars = PopoverCSSVars ,
160+ triggerRect ?: DOMRect ,
161+ boundaryRect ?: DOMRect ,
162+ offsets : ManualOffsets = ZERO_OFFSETS
132163) : PopoverPositionStyle {
133164 const SIDE_OFFSET_VAR = `var(${ cssVars . sideOffset } , 0px)` ;
134165 const ALIGN_OFFSET_VAR = `var(${ cssVars . alignOffset } , 0px)` ;
135166 const { side, align } = opts ;
167+ const boundaryOffset = offsets . boundaryOffset ?? 0 ;
136168 const style : PopoverPositionStyle = {
137169 positionAnchor : `--${ anchorName } ` ,
138170 position : 'fixed' ,
@@ -146,6 +178,7 @@ function getAnchorPositionCSS(
146178 alignSelf : 'normal' ,
147179 marginInlineStart : '0' ,
148180 marginBlockStart : '0' ,
181+ translate : 'none' ,
149182 } ;
150183
151184 // The CSS inset property is the OPPOSITE of the desired side.
@@ -158,6 +191,24 @@ function getAnchorPositionCSS(
158191 if ( side === 'top' || side === 'bottom' ) {
159192 style [ insetProp ] = `calc(anchor(${ side } ) + ${ SIDE_OFFSET_VAR } )` ;
160193
194+ if ( triggerRect && boundaryRect ) {
195+ const { base, translate } = getAnchorCrossAxisShift (
196+ triggerRect . left ,
197+ triggerRect . right ,
198+ triggerRect . width ,
199+ boundaryRect . left ,
200+ boundaryRect . right ,
201+ align ,
202+ offsets . alignOffset ,
203+ boundaryOffset
204+ ) ;
205+
206+ style . left = base ;
207+ style . translate = `${ translate } 0` ;
208+
209+ return style ;
210+ }
211+
161212 // Alignment along the cross axis
162213 if ( align === 'start' ) {
163214 style . left = `calc(anchor(left) + ${ ALIGN_OFFSET_VAR } )` ;
@@ -170,6 +221,24 @@ function getAnchorPositionCSS(
170221 } else {
171222 style [ insetProp ] = `calc(anchor(${ side } ) + ${ SIDE_OFFSET_VAR } )` ;
172223
224+ if ( triggerRect && boundaryRect ) {
225+ const { base, translate } = getAnchorCrossAxisShift (
226+ triggerRect . top ,
227+ triggerRect . bottom ,
228+ triggerRect . height ,
229+ boundaryRect . top ,
230+ boundaryRect . bottom ,
231+ align ,
232+ offsets . alignOffset ,
233+ boundaryOffset
234+ ) ;
235+
236+ style . top = base ;
237+ style . translate = `0 ${ translate } ` ;
238+
239+ return style ;
240+ }
241+
173242 if ( align === 'start' ) {
174243 style . top = `calc(anchor(top) + ${ ALIGN_OFFSET_VAR } )` ;
175244 } else if ( align === 'end' ) {
@@ -280,7 +349,8 @@ export function getManualPositionStyle(
280349 triggerRect : DOMRect ,
281350 popupRect : DOMRect ,
282351 opts : PositioningOptions ,
283- offsets : ManualOffsets = { sideOffset : 0 , alignOffset : 0 }
352+ offsets : ManualOffsets = { sideOffset : 0 , alignOffset : 0 } ,
353+ boundaryRect ?: DOMRect
284354) {
285355 const { side, align } = opts ;
286356 const { sideOffset, alignOffset } = offsets ;
@@ -318,6 +388,26 @@ export function getManualPositionStyle(
318388 }
319389 }
320390
391+ if ( boundaryRect ) {
392+ const boundaryOffset = offsets . boundaryOffset ?? 0 ;
393+
394+ if ( side === 'top' || side === 'bottom' ) {
395+ left = shiftCrossAxis (
396+ left ,
397+ boundaryRect . left + boundaryOffset ,
398+ boundaryRect . right - boundaryOffset ,
399+ popupRect . width
400+ ) ;
401+ } else {
402+ top = shiftCrossAxis (
403+ top ,
404+ boundaryRect . top + boundaryOffset ,
405+ boundaryRect . bottom - boundaryOffset ,
406+ popupRect . height
407+ ) ;
408+ }
409+ }
410+
321411 return {
322412 top : `${ top } px` ,
323413 left : `${ left } px` ,
0 commit comments