@@ -53,3 +53,97 @@ export const findFirstFocusableChild = (element: HTMLElement) =>
5353 element . querySelector < HTMLElement > (
5454 'a[href], button:not([disabled]):not([aria-hidden]), [tabindex]:not([tabindex^="-"])'
5555 ) ;
56+
57+ // Translates an element vertically by a given offset,
58+ // relatively to its current translation.
59+ const translateElementY = (
60+ element : HTMLElement ,
61+ offset : number ,
62+ direction ?: 1 | - 1
63+ ) => {
64+ const base = Number ( element . dataset . translation ) || 0 ;
65+ const translation = base + offset * direction ;
66+
67+ // We're only moving the element if the translation has
68+ // the given direction, as we don't want it to move the
69+ // other way.
70+ if ( Math . sign ( translation ) === Math . sign ( direction ) ) {
71+ element . dataset . translation = translation . toString ( ) ;
72+ element . style . transform = `translateY(${ translation } px)` ;
73+ } else {
74+ delete element . dataset . translation ;
75+ element . style . transform = '' ;
76+ }
77+ } ;
78+
79+ const getCollisionPadding = ( element : HTMLElement ) => {
80+ const styles = window . getComputedStyle ( element ) ;
81+ const padding = styles . getPropertyValue ( '--orejime-collision-padding' ) ;
82+ return padding . length ? parseInt ( padding , 10 ) : 16 ;
83+ } ;
84+
85+ const getPaddedBoundingBox = ( element : DOMRect , padding : number ) => ( {
86+ top : element . top + padding ,
87+ right : element . right + padding ,
88+ bottom : element . bottom + padding ,
89+ left : element . left + padding
90+ } ) ;
91+
92+ // Resolves a visual collision between two elements, either
93+ // by scrolling the page or moving one of them.
94+ // We're only resolving collisions on the vertical axis, as
95+ // it is the main direction of web pages.
96+ export const resolveCollision = ( fixed : HTMLElement , mobile : HTMLElement ) => {
97+ if ( mobile . contains ( fixed ) ) {
98+ translateElementY ( mobile , 0 ) ;
99+ return ;
100+ }
101+
102+ // We're padding the fixed element's bounding box to
103+ // avoid snapping the mobile one right on its border.
104+ const fixedRect = getPaddedBoundingBox (
105+ fixed . getBoundingClientRect ( ) ,
106+ getCollisionPadding ( mobile )
107+ ) ;
108+
109+ const mobileRect = mobile . getBoundingClientRect ( ) ;
110+ const isCollidingX =
111+ mobileRect . left < fixedRect . right && mobileRect . right > fixedRect . left ;
112+
113+ if ( ! isCollidingX ) {
114+ translateElementY ( mobile , 0 ) ;
115+ return ;
116+ }
117+
118+ const mobileCenterY = mobileRect . top + mobileRect . height / 2 ;
119+ const direction = mobileCenterY > window . innerHeight / 2 ? 1 : - 1 ;
120+ const overlap =
121+ direction > 0
122+ ? fixedRect . bottom - mobileRect . top
123+ : mobileRect . bottom - fixedRect . top ;
124+
125+ const isCollidingY =
126+ mobileRect . top < fixedRect . bottom && mobileRect . bottom > fixedRect . top ;
127+
128+ if ( ! isCollidingY ) {
129+ translateElementY ( mobile , overlap , direction ) ;
130+ return ;
131+ }
132+
133+ const doc = document . documentElement ;
134+ const leeway =
135+ direction > 0
136+ ? Math . abs ( doc . scrollHeight - doc . clientHeight - doc . scrollTop )
137+ : doc . scrollTop ;
138+
139+ // We're scrolling as much possible first.
140+ window . scrollBy ( {
141+ top : overlap * direction
142+ } ) ;
143+
144+ // If scrolling isn't enough to get out of trouble,
145+ // we're moving the mobile element out of the way.
146+ if ( overlap > leeway ) {
147+ translateElementY ( mobile , overlap - leeway , direction ) ;
148+ }
149+ } ;
0 commit comments