@@ -2,7 +2,8 @@ import TestCase from '../../TestCase';
22import Fixture from '../../Fixture' ;
33
44const React = window . React ;
5- const { Fragment, useRef, useState} = React ;
5+ const { Fragment, useRef, useState, useEffect} = React ;
6+ const ReactDOM = window . ReactDOM ;
67
78function Controls ( {
89 alignToTop,
@@ -48,8 +49,12 @@ function TargetElement({color, top, id}) {
4849
4950export default function ScrollIntoViewCase ( ) {
5051 const [ alignToTop , setAlignToTop ] = useState ( true ) ;
52+ const [ displayFixedElements , setDisplayFixedElements ] = useState ( false ) ;
53+ const [ didMount , setDidMount ] = useState ( false ) ;
5154 const verticalRef = useRef ( null ) ;
5255 const noChildRef = useRef ( null ) ;
56+ const testCaseRef = useRef ( null ) ;
57+ const scrollContainerRef = useRef ( null ) ;
5358
5459 const scrollVertical = ( ) => {
5560 verticalRef . current . scrollIntoView ( alignToTop ) ;
@@ -59,69 +64,124 @@ export default function ScrollIntoViewCase() {
5964 noChildRef . current . scrollIntoView ( alignToTop ) ;
6065 } ;
6166
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+
6295 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 >
100134 < 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" />
104145 </ 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 >
126186 ) ;
127187}
0 commit comments