@@ -11,103 +11,154 @@ export function handleIOSLocking(): ScrollLockStep<ContainerMetadata> {
11
11
return { }
12
12
}
13
13
14
- let scrollPosition : number
15
-
16
14
return {
17
- before ( ) {
18
- scrollPosition = window . pageYOffset
19
- } ,
20
-
21
- after ( { doc, d, meta } ) {
15
+ before ( { doc, d, meta } ) {
22
16
function inAllowedContainer ( el : HTMLElement ) {
23
17
return meta . containers
24
18
. flatMap ( ( resolve ) => resolve ( ) )
25
19
. some ( ( container ) => container . contains ( el ) )
26
20
}
27
21
28
- // We need to be able to offset the body with the current scroll position. However, if you
29
- // have `scroll-behavior: smooth` set, then changing the scrollTop in any way shape or form
30
- // will trigger a "smooth" scroll and the new position would be incorrect.
31
- //
32
- // This is why we are forcing the `scroll-behaviour: auto` here, and then restoring it later.
33
- // We have to be a bit careful, because removing `scroll-behavior: auto` back to
34
- // `scroll-behavior: smooth` can start triggering smooth scrolling. Delaying this by a
35
- // microTask will guarantee that everything is done such that both enter/exit of the Dialog is
36
- // not using smooth scrolling.
37
- if ( window . getComputedStyle ( doc . documentElement ) . scrollBehavior !== 'auto' ) {
38
- let _d = disposables ( )
39
- _d . style ( doc . documentElement , 'scroll-behavior' , 'auto' )
40
- d . add ( ( ) => d . microTask ( ( ) => _d . dispose ( ) ) )
41
- }
22
+ d . microTask ( ( ) => {
23
+ // We need to be able to offset the body with the current scroll position. However, if you
24
+ // have `scroll-behavior: smooth` set, then changing the scrollTop in any way shape or form
25
+ // will trigger a "smooth" scroll and the new position would be incorrect.
26
+ //
27
+ // This is why we are forcing the `scroll-behaviour: auto` here, and then restoring it later.
28
+ // We have to be a bit careful, because removing `scroll-behavior: auto` back to
29
+ // `scroll-behavior: smooth` can start triggering smooth scrolling. Delaying this by a
30
+ // microTask will guarantee that everything is done such that both enter/exit of the Dialog is
31
+ // not using smooth scrolling.
32
+ if ( window . getComputedStyle ( doc . documentElement ) . scrollBehavior !== 'auto' ) {
33
+ let _d = disposables ( )
34
+ _d . style ( doc . documentElement , 'scrollBehavior' , 'auto' )
35
+ d . add ( ( ) => d . microTask ( ( ) => _d . dispose ( ) ) )
36
+ }
37
+
38
+ // Keep track of the current scroll position so that we can restore the scroll position if
39
+ // it has changed in the meantime.
40
+ let scrollPosition = window . scrollY ?? window . pageYOffset
41
+
42
+ // Relatively hacky, but if you click a link like `<a href="#foo">` in the Dialog, and there
43
+ // exists an element on the page (outside of the Dialog) with that id, then the browser will
44
+ // scroll to that position. However, this is not the case if the element we want to scroll to
45
+ // is higher and the browser needs to scroll up, but it doesn't do that.
46
+ //
47
+ // Let's try and capture that element and store it, so that we can later scroll to it once the
48
+ // Dialog closes.
49
+ let scrollToElement : HTMLElement | null = null
50
+ d . addEventListener (
51
+ doc ,
52
+ 'click' ,
53
+ ( e ) => {
54
+ if ( ! ( e . target instanceof HTMLElement ) ) {
55
+ return
56
+ }
57
+
58
+ try {
59
+ let anchor = e . target . closest ( 'a' )
60
+ if ( ! anchor ) return
61
+ let { hash } = new URL ( anchor . href )
62
+ let el = doc . querySelector ( hash )
63
+ if ( el && ! inAllowedContainer ( el as HTMLElement ) ) {
64
+ scrollToElement = el as HTMLElement
65
+ }
66
+ } catch ( err ) { }
67
+ } ,
68
+ true
69
+ )
70
+
71
+ // Rely on overscrollBehavior to prevent scrolling outside of the Dialog.
72
+ d . addEventListener ( doc , 'touchstart' , ( e ) => {
73
+ if ( e . target instanceof HTMLElement ) {
74
+ if ( inAllowedContainer ( e . target as HTMLElement ) ) {
75
+ // Find the root of the allowed containers
76
+ let rootContainer = e . target
77
+ while (
78
+ rootContainer . parentElement &&
79
+ inAllowedContainer ( rootContainer . parentElement )
80
+ ) {
81
+ rootContainer = rootContainer . parentElement !
82
+ }
42
83
43
- d . style ( doc . body , 'marginTop' , `-${ scrollPosition } px` )
44
- window . scrollTo ( 0 , 0 )
45
-
46
- // Relatively hacky, but if you click a link like `<a href="#foo">` in the Dialog, and there
47
- // exists an element on the page (outside of the Dialog) with that id, then the browser will
48
- // scroll to that position. However, this is not the case if the element we want to scroll to
49
- // is higher and the browser needs to scroll up, but it doesn't do that.
50
- //
51
- // Let's try and capture that element and store it, so that we can later scroll to it once the
52
- // Dialog closes.
53
- let scrollToElement : HTMLElement | null = null
54
- d . addEventListener (
55
- doc ,
56
- 'click' ,
57
- ( e ) => {
58
- if ( ! ( e . target instanceof HTMLElement ) ) {
59
- return
84
+ d . style ( rootContainer , 'overscrollBehavior' , 'contain' )
85
+ } else {
86
+ d . style ( e . target , 'touchAction' , 'none' )
87
+ }
60
88
}
89
+ } )
90
+
91
+ d . addEventListener (
92
+ doc ,
93
+ 'touchmove' ,
94
+ ( e ) => {
95
+ // Check if we are scrolling inside any of the allowed containers, if not let's cancel the event!
96
+ if ( e . target instanceof HTMLElement ) {
97
+ if ( inAllowedContainer ( e . target as HTMLElement ) ) {
98
+ // Even if we are in an allowed container, on iOS the main page can still scroll, we
99
+ // have to make sure that we `event.preventDefault()` this event to prevent that.
100
+ //
101
+ // However, if we happen to scroll on an element that is overflowing, or any of its
102
+ // parents are overflowing, then we should not call `event.preventDefault()` because
103
+ // otherwise we are preventing the user from scrolling inside that container which
104
+ // is not what we want.
105
+ let scrollableParent = e . target
106
+ while (
107
+ scrollableParent . parentElement &&
108
+ // Assumption: We are always used in a Headless UI Portal. Once we reach the
109
+ // portal itself, we can stop crawling up the tree.
110
+ scrollableParent . dataset . headlessuiPortal !== ''
111
+ ) {
112
+ // Check if the scrollable container is overflowing or not.
113
+ //
114
+ // NOTE: we could check the `overflow`, `overflow-y` and `overflow-x` properties
115
+ // but when there is no overflow happening then the `overscrollBehavior` doesn't
116
+ // seem to work and the main page will still scroll. So instead we check if the
117
+ // scrollable container is overflowing or not and use that heuristic instead.
118
+ if (
119
+ scrollableParent . scrollHeight > scrollableParent . clientHeight ||
120
+ scrollableParent . scrollWidth > scrollableParent . clientWidth
121
+ ) {
122
+ break
123
+ }
124
+
125
+ scrollableParent = scrollableParent . parentElement
126
+ }
61
127
62
- try {
63
- let anchor = e . target . closest ( 'a' )
64
- if ( ! anchor ) return
65
- let { hash } = new URL ( anchor . href )
66
- let el = doc . querySelector ( hash )
67
- if ( el && ! inAllowedContainer ( el as HTMLElement ) ) {
68
- scrollToElement = el as HTMLElement
128
+ // We crawled up the tree until the beginnging of the Portal, let's prevent the
129
+ // event if this is the case. If not, then we are in a container where we are
130
+ // allowed to scroll so we don't have to prevent the event.
131
+ if ( scrollableParent . dataset . headlessuiPortal === '' ) {
132
+ e . preventDefault ( )
133
+ }
134
+ }
135
+
136
+ // We are not in an allowed container, so let's prevent the event.
137
+ else {
138
+ e . preventDefault ( )
139
+ }
69
140
}
70
- } catch ( err ) { }
71
- } ,
72
- true
73
- )
74
-
75
- d . addEventListener (
76
- doc ,
77
- 'touchmove' ,
78
- ( e ) => {
79
- // Check if we are scrolling inside any of the allowed containers, if not let's cancel the event!
80
- if ( e . target instanceof HTMLElement && ! inAllowedContainer ( e . target as HTMLElement ) ) {
81
- e . preventDefault ( )
141
+ } ,
142
+ { passive : false }
143
+ )
144
+
145
+ // Restore scroll position if a scrollToElement was captured.
146
+ d . add ( ( ) => {
147
+ let newScrollPosition = window . scrollY ?? window . pageYOffset
148
+
149
+ // If the scroll position changed, then we can restore it to the previous value. This will
150
+ // happen if you focus an input field and the browser scrolls for you.
151
+ if ( scrollPosition !== newScrollPosition ) {
152
+ window . scrollTo ( 0 , scrollPosition )
82
153
}
83
- } ,
84
- { passive : false }
85
- )
86
-
87
- // Restore scroll position
88
- d . add ( ( ) => {
89
- // Before opening the Dialog, we capture the current pageYOffset, and offset the page with
90
- // this value so that we can also scroll to `(0, 0)`.
91
- //
92
- // If we want to restore a few things can happen:
93
- //
94
- // 1. The window.pageYOffset is still at 0, this means nothing happened, and we can safely
95
- // restore to the captured value earlier.
96
- // 2. The window.pageYOffset is **not** at 0. This means that something happened (e.g.: a
97
- // link was scrolled into view in the background). Ideally we want to restore to this _new_
98
- // position. To do this, we can take the new value into account with the captured value from
99
- // before.
100
- //
101
- // (Since the value of window.pageYOffset is 0 in the first case, we should be able to
102
- // always sum these values)
103
- window . scrollTo ( 0 , window . pageYOffset + scrollPosition )
104
-
105
- // If we captured an element that should be scrolled to, then we can try to do that if the
106
- // element is still connected (aka, still in the DOM).
107
- if ( scrollToElement && scrollToElement . isConnected ) {
108
- scrollToElement . scrollIntoView ( { block : 'nearest' } )
109
- scrollToElement = null
110
- }
154
+
155
+ // If we captured an element that should be scrolled to, then we can try to do that if the
156
+ // element is still connected (aka, still in the DOM).
157
+ if ( scrollToElement && scrollToElement . isConnected ) {
158
+ scrollToElement . scrollIntoView ( { block : 'nearest' } )
159
+ scrollToElement = null
160
+ }
161
+ } )
111
162
} )
112
163
} ,
113
164
}
0 commit comments