Skip to content

Commit 51e0162

Browse files
committed
Update container grouping based on parent scroll container
1 parent 805336f commit 51e0162

File tree

5 files changed

+425
-239
lines changed

5 files changed

+425
-239
lines changed

fixtures/dom/src/components/fixtures/fragment-refs/ScrollIntoViewCase.js

+122-62
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import TestCase from '../../TestCase';
22
import Fixture from '../../Fixture';
33

44
const React = window.React;
5-
const {Fragment, useRef, useState} = React;
5+
const {Fragment, useRef, useState, useEffect} = React;
6+
const ReactDOM = window.ReactDOM;
67

78
function Controls({
89
alignToTop,
@@ -48,8 +49,12 @@ function TargetElement({color, top, id}) {
4849

4950
export 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
}

fixtures/dom/src/index.js

+12-3
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,23 @@ import './polyfills';
22
import loadReact, {isLocal} from './react-loader';
33

44
if (isLocal()) {
5-
Promise.all([import('react'), import('react-dom/client')])
6-
.then(([React, ReactDOMClient]) => {
7-
if (React === undefined || ReactDOMClient === undefined) {
5+
Promise.all([
6+
import('react'),
7+
import('react-dom'),
8+
import('react-dom/client'),
9+
])
10+
.then(([React, ReactDOM, ReactDOMClient]) => {
11+
if (
12+
React === undefined ||
13+
ReactDOM === undefined ||
14+
ReactDOMClient === undefined
15+
) {
816
throw new Error(
917
'Unable to load React. Build experimental and then run `yarn dev` again'
1018
);
1119
}
1220
window.React = React;
21+
window.ReactDOM = ReactDOM;
1322
window.ReactDOMClient = ReactDOMClient;
1423
})
1524
.then(() => import('./components/App'))

0 commit comments

Comments
 (0)