Skip to content

Add scrollIntoView to fragment instances #32814

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import TestCase from '../../TestCase';
import Fixture from '../../Fixture';

const React = window.React;
const {Fragment, useRef, useState} = React;

function WrapperComponent(props) {
return props.children;
}

function handler(e) {
const text = e.currentTarget.innerText;
alert('You clicked: ' + text);
}

const initialState = {
child: false,
parent: false,
grandparent: false,
};

export default function EventListenerCase() {
const fragmentRef = useRef(null);
const [clickedState, setClickedState] = useState({...initialState});

return (
<TestCase title="Event Dispatch">
<TestCase.Steps>
<li>
Each div has regular click handlers, you can click each one to observe
the status changing
</li>
<li>Clear the clicked state</li>
<li>
Click the "Dispatch click event" button to dispatch a click event on
the Fragment
</li>
</TestCase.Steps>

<TestCase.ExpectedResult>
Dispatching an event on a Fragment will forward the dispatch to its
parent. You can observe when dispatching that the parent handler is
called in additional to bubbling from there. A delay is added to make
the bubbling more clear.
</TestCase.ExpectedResult>

<Fixture>
<Fixture.Controls>
<button
onClick={() => {
fragmentRef.current.dispatchEvent(
new MouseEvent('click', {bubbles: true})
);
}}>
Dispatch click event
</button>
<button
onClick={() => {
setClickedState({...initialState});
}}>
Reset clicked state
</button>
</Fixture.Controls>
<div
onClick={() => {
setTimeout(() => {
setClickedState(prev => ({...prev, grandparent: true}));
}, 200);
}}
className="card">
Fragment grandparent - clicked:{' '}
{clickedState.grandparent ? 'true' : 'false'}
<div
onClick={() => {
setTimeout(() => {
setClickedState(prev => ({...prev, parent: true}));
}, 100);
}}
className="card">
Fragment parent - clicked: {clickedState.parent ? 'true' : 'false'}
<Fragment ref={fragmentRef}>
<div
className="card"
onClick={() => {
setClickedState(prev => ({...prev, child: true}));
}}>
Fragment child - clicked:{' '}
{clickedState.child ? 'true' : 'false'}
</div>
</Fragment>
</div>
</div>
</Fixture>
</TestCase>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Fixture from '../../Fixture';

const React = window.React;

const {Fragment, useEffect, useRef, useState} = React;
const {Fragment, useRef} = React;

export default function FocusCase() {
const fragmentRef = useRef(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import TestCase from '../../TestCase';
import Fixture from '../../Fixture';

const React = window.React;
const {Fragment, useEffect, useRef, useState} = React;
const {Fragment, useRef, useState} = React;

export default function GetClientRectsCase() {
const fragmentRef = useRef(null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import TestCase from '../../TestCase';
import Fixture from '../../Fixture';

const React = window.React;
const {Fragment, useRef, useState} = React;

function Controls({
alignToTop,
setAlignToTop,
scrollVertical,
scrollVerticalNoChildren,
}) {
return (
<div>
<label>
Align to Top:
<input
type="checkbox"
checked={alignToTop}
onChange={e => setAlignToTop(e.target.checked)}
/>
</label>
<div>
<button onClick={scrollVertical}>scrollIntoView() - Vertical</button>
<button onClick={scrollVerticalNoChildren}>
scrollIntoView() - Vertical, No children
</button>
</div>
</div>
);
}

function TargetElement({color, top, id}) {
return (
<div
id={id}
style={{
height: 500,
backgroundColor: color,
marginTop: top ? '50vh' : 0,
marginBottom: 100,
flexShrink: 0,
}}>
{id}
</div>
);
}

export default function ScrollIntoViewCase() {
const [alignToTop, setAlignToTop] = useState(true);
const verticalRef = useRef(null);
const noChildRef = useRef(null);

const scrollVertical = () => {
verticalRef.current.scrollIntoView(alignToTop);
};

const scrollVerticalNoChildren = () => {
noChildRef.current.scrollIntoView(alignToTop);
};

return (
<TestCase title="ScrollIntoView">
<TestCase.Steps>
<li>Toggle alignToTop and click the buttons to scroll</li>
</TestCase.Steps>
<TestCase.ExpectedResult>
<p>When the Fragment has children:</p>
<p>
The simple path is that all children are in the same scroll container.
If alignToTop=true|undefined, we will select the first Fragment host
child to call scrollIntoView on. Otherwise we'll call on the last host
child.
</p>
<p>
In the case of fixed or sticky elements and portals (we have here
sticky header and footer), we split up the host children into groups
of scroll containers. If we hit a sticky/fixed element, we'll always
attempt to scroll on the first or last element of the next group.
</p>
<p>When the Fragment does not have children:</p>
<p>
The Fragment still represents a virtual space. We can scroll to the
nearest edge by selecting the host sibling before if alignToTop=false,
or after if alignToTop=true|undefined. We'll fall back to the other
sibling or parent in the case that the preferred sibling target
doesn't exist.
</p>
</TestCase.ExpectedResult>
<Fixture>
<Fixture.Controls>
<Controls
alignToTop={alignToTop}
setAlignToTop={setAlignToTop}
scrollVertical={scrollVertical}
scrollVerticalNoChildren={scrollVerticalNoChildren}
/>
</Fixture.Controls>
<Fragment ref={verticalRef}>
<div
style={{position: 'sticky', top: 100, backgroundColor: 'red'}}
id="header">
Sticky header
</div>
<TargetElement color="lightgreen" top={true} id="A" />
<Fragment ref={noChildRef}></Fragment>
<TargetElement color="lightcoral" id="B" />
<TargetElement color="lightblue" id="C" />
<div
style={{position: 'sticky', bottom: 0, backgroundColor: 'purple'}}
id="footer">
Sticky footer
</div>
</Fragment>

<Fixture.Controls>
<Controls
alignToTop={alignToTop}
setAlignToTop={setAlignToTop}
scrollVertical={scrollVertical}
scrollVerticalNoChildren={scrollVerticalNoChildren}
/>
</Fixture.Controls>
</Fixture>
</TestCase>
);
}
4 changes: 4 additions & 0 deletions fixtures/dom/src/components/fixtures/fragment-refs/index.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import FixtureSet from '../../FixtureSet';
import EventListenerCase from './EventListenerCase';
import EventDispatchCase from './EventDispatchCase';
import IntersectionObserverCase from './IntersectionObserverCase';
import ResizeObserverCase from './ResizeObserverCase';
import FocusCase from './FocusCase';
import GetClientRectsCase from './GetClientRectsCase';
import ScrollIntoViewCase from './ScrollIntoViewCase';

const React = window.React;

export default function FragmentRefsPage() {
return (
<FixtureSet title="Fragment Refs">
<EventListenerCase />
<EventDispatchCase />
<IntersectionObserverCase />
<ResizeObserverCase />
<FocusCase />
<GetClientRectsCase />
<ScrollIntoViewCase />
</FixtureSet>
);
}
Loading