@@ -2,7 +2,8 @@ import TestCase from '../../TestCase';
2
2
import Fixture from '../../Fixture' ;
3
3
4
4
const React = window . React ;
5
- const { Fragment, useRef, useState} = React ;
5
+ const { Fragment, useRef, useState, useEffect} = React ;
6
+ const ReactDOM = window . ReactDOM ;
6
7
7
8
function Controls ( {
8
9
alignToTop,
@@ -48,8 +49,12 @@ function TargetElement({color, top, id}) {
48
49
49
50
export default function ScrollIntoViewCase ( ) {
50
51
const [ alignToTop , setAlignToTop ] = useState ( true ) ;
52
+ const [ displayFixedElements , setDisplayFixedElements ] = useState ( false ) ;
53
+ const [ didMount , setDidMount ] = useState ( false ) ;
51
54
const verticalRef = useRef ( null ) ;
52
55
const noChildRef = useRef ( null ) ;
56
+ const testCaseRef = useRef ( null ) ;
57
+ const scrollContainerRef = useRef ( null ) ;
53
58
54
59
const scrollVertical = ( ) => {
55
60
verticalRef . current . scrollIntoView ( alignToTop ) ;
@@ -59,69 +64,124 @@ export default function ScrollIntoViewCase() {
59
64
noChildRef . current . scrollIntoView ( alignToTop ) ;
60
65
} ;
61
66
67
+ // Hack to portal child into the scroll container
68
+ // after the first render. This is to simulate a case where
69
+ // an item is portaled into another scroll container.
70
+ useEffect ( ( ) => {
71
+ if ( ! didMount ) {
72
+ setDidMount ( true ) ;
73
+ }
74
+ } , [ ] ) ;
75
+
76
+ useEffect ( ( ) => {
77
+ const observer = new IntersectionObserver ( entries => {
78
+ entries . forEach ( entry => {
79
+ if ( entry . isIntersecting ) {
80
+ setDisplayFixedElements ( true ) ;
81
+ } else {
82
+ setDisplayFixedElements ( false ) ;
83
+ }
84
+ } ) ;
85
+ } ) ;
86
+ testCaseRef . current . observeUsing ( observer ) ;
87
+
88
+ const lastRef = testCaseRef . current ;
89
+ return ( ) => {
90
+ lastRef . unobserveUsing ( observer ) ;
91
+ observer . disconnect ( ) ;
92
+ } ;
93
+ } ) ;
94
+
62
95
return (
63
- < TestCase title = "ScrollIntoView" >
64
- < TestCase . Steps >
65
- < li > Toggle alignToTop and click the buttons to scroll</ li >
66
- </ TestCase . Steps >
67
- < TestCase . ExpectedResult >
68
- < p > When the Fragment has children:</ p >
69
- < p >
70
- The simple path is that all children are in the same scroll container.
71
- If alignToTop=true|undefined, we will select the first Fragment host
72
- child to call scrollIntoView on. Otherwise we'll call on the last host
73
- child.
74
- </ p >
75
- < p >
76
- In the case of fixed or sticky elements and portals (we have here
77
- sticky header and footer), we split up the host children into groups
78
- of scroll containers. If we hit a sticky/fixed element, we'll always
79
- attempt to scroll on the first or last element of the next group.
80
- </ p >
81
- < p > When the Fragment does not have children:</ p >
82
- < p >
83
- The Fragment still represents a virtual space. We can scroll to the
84
- nearest edge by selecting the host sibling before if alignToTop=false,
85
- or after if alignToTop=true|undefined. We'll fall back to the other
86
- sibling or parent in the case that the preferred sibling target
87
- doesn't exist.
88
- </ p >
89
- </ TestCase . ExpectedResult >
90
- < Fixture >
91
- < Fixture . Controls >
92
- < Controls
93
- alignToTop = { alignToTop }
94
- setAlignToTop = { setAlignToTop }
95
- scrollVertical = { scrollVertical }
96
- scrollVerticalNoChildren = { scrollVerticalNoChildren }
97
- />
98
- </ Fixture . Controls >
99
- < Fragment ref = { verticalRef } >
96
+ < Fragment ref = { testCaseRef } >
97
+ < TestCase title = "ScrollIntoView" >
98
+ < TestCase . Steps >
99
+ < li > Toggle alignToTop and click the buttons to scroll</ li >
100
+ </ TestCase . Steps >
101
+ < TestCase . ExpectedResult >
102
+ < p > When the Fragment has children:</ p >
103
+ < p >
104
+ The simple path is that all children are in the same scroll
105
+ container. If alignToTop=true|undefined, we will select the first
106
+ Fragment host child to call scrollIntoView on. Otherwise we'll call
107
+ on the last host child.
108
+ </ p >
109
+ < p >
110
+ In the case of fixed elements and inserted elements or portals
111
+ causing fragment siblings to be in different scroll containers, we
112
+ split up the host children into groups of scroll containers. If we
113
+ hit a fixed element, we'll always attempt to scroll on the first or
114
+ last element of the next group.
115
+ </ p >
116
+ < p > When the Fragment does not have children:</ p >
117
+ < p >
118
+ The Fragment still represents a virtual space. We can scroll to the
119
+ nearest edge by selecting the host sibling before if
120
+ alignToTop=false, or after if alignToTop=true|undefined. We'll fall
121
+ back to the other sibling or parent in the case that the preferred
122
+ sibling target doesn't exist.
123
+ </ p >
124
+ </ TestCase . ExpectedResult >
125
+ < Fixture >
126
+ < Fixture . Controls >
127
+ < Controls
128
+ alignToTop = { alignToTop }
129
+ setAlignToTop = { setAlignToTop }
130
+ scrollVertical = { scrollVertical }
131
+ scrollVerticalNoChildren = { scrollVerticalNoChildren }
132
+ />
133
+ </ Fixture . Controls >
100
134
< div
101
- style = { { position : 'sticky' , top : 100 , backgroundColor : 'red' } }
102
- id = "header" >
103
- Sticky header
135
+ style = { {
136
+ height : '50vh' ,
137
+ overflowY : 'auto' ,
138
+ border : '1px solid black' ,
139
+ marginBottom : '1rem' ,
140
+ } }
141
+ ref = { scrollContainerRef } >
142
+ < TargetElement color = "lightyellow" id = "SCROLLABLE-1" />
143
+ < TargetElement color = "lightpink" id = "SCROLLABLE-2" />
144
+ < TargetElement color = "lightcyan" id = "SCROLLABLE-3" />
104
145
</ div >
105
- < TargetElement color = "lightgreen" top = { true } id = "A" />
106
- < Fragment ref = { noChildRef } > </ Fragment >
107
- < TargetElement color = "lightcoral" id = "B" />
108
- < TargetElement color = "lightblue" id = "C" />
109
- < div
110
- style = { { position : 'sticky' , bottom : 0 , backgroundColor : 'purple' } }
111
- id = "footer" >
112
- Sticky footer
113
- </ div >
114
- </ Fragment >
115
-
116
- < Fixture . Controls >
117
- < Controls
118
- alignToTop = { alignToTop }
119
- setAlignToTop = { setAlignToTop }
120
- scrollVertical = { scrollVertical }
121
- scrollVerticalNoChildren = { scrollVerticalNoChildren }
122
- />
123
- </ Fixture . Controls >
124
- </ Fixture >
125
- </ TestCase >
146
+ < Fragment ref = { verticalRef } >
147
+ { displayFixedElements && (
148
+ < div
149
+ style = { { position : 'fixed' , top : 0 , backgroundColor : 'red' } }
150
+ id = "header" >
151
+ Fixed header
152
+ </ div >
153
+ ) }
154
+ { didMount &&
155
+ ReactDOM . createPortal (
156
+ < TargetElement color = "red" id = "SCROLLABLE-4" /> ,
157
+ scrollContainerRef . current
158
+ ) }
159
+ < TargetElement color = "lightgreen" top = { true } id = "A" />
160
+ < Fragment ref = { noChildRef } > </ Fragment >
161
+ < TargetElement color = "lightcoral" id = "B" />
162
+ < TargetElement color = "lightblue" id = "C" />
163
+ { displayFixedElements && (
164
+ < div
165
+ style = { {
166
+ position : 'fixed' ,
167
+ bottom : 0 ,
168
+ backgroundColor : 'purple' ,
169
+ } }
170
+ id = "footer" >
171
+ Fixed footer
172
+ </ div >
173
+ ) }
174
+ </ Fragment >
175
+ < Fixture . Controls >
176
+ < Controls
177
+ alignToTop = { alignToTop }
178
+ setAlignToTop = { setAlignToTop }
179
+ scrollVertical = { scrollVertical }
180
+ scrollVerticalNoChildren = { scrollVerticalNoChildren }
181
+ />
182
+ </ Fixture . Controls >
183
+ </ Fixture >
184
+ </ TestCase >
185
+ </ Fragment >
126
186
) ;
127
187
}
0 commit comments