@@ -53,3 +53,73 @@ 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+ // Resolves a visual collision between two elements, either
80+ // by scrolling the page or moving one of them.
81+ // We're only resolving collisions on the vertical axis, as
82+ // it is the main direction of web pages.
83+ export const resolveCollision = ( fixed : HTMLElement , mobile : HTMLElement ) => {
84+ if ( mobile . contains ( fixed ) ) {
85+ translateElementY ( mobile , 0 ) ;
86+ return ;
87+ }
88+
89+ const fixedRect = fixed . getBoundingClientRect ( ) ;
90+ const mobileRect = mobile . getBoundingClientRect ( ) ;
91+ const isColliding =
92+ mobileRect . left < fixedRect . right
93+ && mobileRect . right > fixedRect . x
94+ && mobileRect . top < fixedRect . bottom
95+ && mobileRect . bottom > fixedRect . top ;
96+
97+ const mobileCenterY = mobileRect . top + mobileRect . height / 2 ;
98+ const direction = mobileCenterY > window . innerHeight / 2 ? 1 : - 1 ;
99+ const overlap =
100+ direction > 0
101+ ? fixedRect . bottom - mobileRect . top
102+ : mobileRect . bottom - fixedRect . top ;
103+
104+ if ( ! isColliding ) {
105+ translateElementY ( mobile , overlap , direction ) ;
106+ return ;
107+ }
108+
109+ const doc = document . documentElement ;
110+ const leeway =
111+ direction > 0
112+ ? Math . abs ( doc . scrollHeight - doc . clientHeight - doc . scrollTop )
113+ : doc . scrollTop ;
114+
115+ // We're scrolling as much possible first.
116+ window . scrollBy ( {
117+ top : overlap * direction
118+ } ) ;
119+
120+ // If scrolling isn't enough to get out of trouble,
121+ // we're moving the mobile element out of the way.
122+ if ( overlap > leeway ) {
123+ translateElementY ( mobile , overlap - leeway , direction ) ;
124+ }
125+ } ;
0 commit comments