Skip to content

Commit

Permalink
feat: Add scrollToFn, align options and getIndexOffset
Browse files Browse the repository at this point in the history
- add scrollToFn
- add align options to scrollToIndex function
- add align options to scrollToOffset function
- add getIndexOffset function
  • Loading branch information
tannerlinsley committed May 9, 2020
1 parent 65e0f3b commit f0e281f
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 20 deletions.
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Enjoy this library? Try them all! [React Table](https://github.com/tannerlinsley
- Row, Column, and Grid virtualization
- One single **headless** hook
- Fixed, variable and dynamic measurement modes
- Imperative scrollTo control for offset, indices and alignment
- Custom scrolling function support (eg. smooth scroll)
- <a href="https://bundlephobia.com/result?p=react-virtual@latest" target="\_parent">
<img alt="" src="https://badgen.net/bundlephobia/minzip/react-virtual@latest" />
</a>
Expand Down Expand Up @@ -264,12 +266,14 @@ const {
totalSize,
scrollToIndex,
scrollToOffset,
getIndexOffset,
scrollToFn,
} = useVirtual({
size,
parentRef,
estimateSize,
overscan,
horiztonal,
horizontal,
})
```

Expand All @@ -290,10 +294,15 @@ const {
- A best-guess size (when using dynamic measurement rendering)
- When this function's memoization changes, the entire list is recalculated
- `overscan: Integer`
- The amount of items to load both behind and ahead of the current window range
- Defaults to `1`
- The amount of items to load both behind and ahead of the current window range
- `horizontal: Boolean`
- Defaults to `false`
- When `true`, this virtualizer will use `width` and `scrollLeft` instead of `height` and `scrollTop` to determine size and offset of virtualized items.
- `scrollToFn: Function(offset) => void 0`
- Optional
- This function, if passed, is responsible for implementing the scrollTo log for the parentRef.
- Eg. You can use this function implement smooth scrolling by using the supplied offset and animating the parentRef's scroll offset appropriately as seen in the sandbox's **Smooth Scroll** example.

### Returns

Expand All @@ -313,10 +322,30 @@ const {
- `totalSize: Integer`
- The total size of the entire virtualizer
- When using dynamic measurement refs, this number may change as items are measured after they are rendered.
- `scrollToIndex: Function(index: Integer) => void 0`
- `scrollToIndex: Function(index: Integer, { align: String }) => void 0`
- Call this function to scroll the top/left of the parentRef element to the start of the item located at the passed index.
- `scrollToOffset: Function(offsetInPixels: Integer) => void 0`
- `align: 'start' | 'center' | 'end' | 'auto'`
- Defaults to `start`
- `start` places the item at the top/left of the visible scroll area
- `center` places the item in the center of the visible scroll area
- `end` places the item at the bottom/right of the visible scroll area
- `auto` brings the item into the visible scroll area either at the start or end, depending on which is closer. If the item is already in view, it is placed at the `top/left` of the visible scroll area.
- `scrollToOffset: Function(offsetInPixels: Integer, { align: String }) => void 0`
- Call this function to scroll the top/left of the parentRef element to the passed pixel offset.
- `align: 'start' | 'center' | 'end' | 'auto'`
- Defaults to `start`
- `start` places the offset at the top/left of the visible scroll area
- `center` places the offset in the center of the visible scroll area
- `end` places the offset at the bottom/right of the visible scroll area
- `auto` brings the offset into the visible scroll area either at the start or end, depending on which is closer. If the offset is already in view, it is placed at the `top/left` of the visible scroll area.
- `getIndexOffset: Function(index: Integer, { align: String }) => void 0`
- Call this function to return the offset of the item located at the passed index.
- `align: 'start' | 'center' | 'end' | 'auto'`
- Defaults to `start`
- `start` returns the item's top/left offset
- `center` returns the item's center offset
- `end` returns the item's bottom/right offset
- `auto` return's the item's start or end offset, depending on which is closer. If the item is already in view, the `top/left` offset will be returned

# Contributors ✨

Expand Down
91 changes: 91 additions & 0 deletions examples/sandbox/src/SmoothScroll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from "react";

import { useVirtual } from "react-virtual";

function easeInOutQuint(t) {
return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t;
}

export default function() {
const parentRef = React.useRef();

const scrollToFn = React.useCallback(offset => {
const duration = 1000;
const start = parentRef.current.scrollTop;
const startTime = Date.now();

const run = () => {
const now = Date.now();
const elapsed = now - startTime;
const progress = easeInOutQuint(Math.min(elapsed / duration, 1));
const interpolated = start + (offset - start) * progress;

if (elapsed < duration) {
parentRef.current.scrollTop = interpolated;
setTimeout(run, 16);
} else {
parentRef.current.scrollTop = interpolated;
}
};

setTimeout(run, 16);
}, []);

const rowVirtualizer = useVirtual({
size: 10000,
parentRef,
estimateSize: React.useCallback(() => 35, []),
overscan: 5,
scrollToFn
});

return (
<>
<button
onClick={() =>
rowVirtualizer.scrollToIndex(Math.floor(Math.random() * 10000))
}
>
Scroll To Random Index
</button>

<br />
<br />

<div
ref={parentRef}
className="List"
style={{
height: `200px`,
width: `400px`,
overflow: "auto"
}}
>
<div
style={{
height: `${rowVirtualizer.totalSize}px`,
width: "100%",
position: "relative"
}}
>
{rowVirtualizer.virtualItems.map(virtualRow => (
<div
key={virtualRow.index}
className={virtualRow.index % 2 ? "ListItemOdd" : "ListItemEven"}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`
}}
>
Row {virtualRow.index}
</div>
))}
</div>
</div>
</>
);
}
13 changes: 13 additions & 0 deletions examples/sandbox/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "./styles.css";
import Fixed from "./Fixed";
import Variable from "./Variable";
import Dynamic from "./Dynamic";
import SmoothScroll from "./SmoothScroll";

function App() {
const [mode, setMode] = React.useState("fixed"); // fixed | variable | dynamic
Expand All @@ -16,6 +17,7 @@ function App() {
<option value="fixed">Fixed</option>
<option value="variable">Variable</option>
<option value="dynamic">Dynamic</option>
<option value="smooth-scroll">Smooth Scroll</option>
</select>

<br />
Expand Down Expand Up @@ -56,6 +58,17 @@ function App() {
<br />
<Dynamic />
</>
) : mode === "smooth-scroll" ? (
<>
<p>
Smooth scroll uses the <code>`scrollToFn`</code> to implement a
custom scrolling function for the methods like{" "}
<code>`scrollToIndex`</code> and <code>`scrollToOffset`</code>
</p>
<br />
<br />
<SmoothScroll />
</>
) : null}
<br />
<br />
Expand Down
108 changes: 92 additions & 16 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,28 @@ import React from 'react'

import useScroll from './useScroll'
import useRect from './useRect'
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'

export function useVirtual({
size = 0,
estimateSize,
overscan = 1,
parentRef,
horizontal,
scrollToFn,
}) {
const sizeKey = horizontal ? 'width' : 'height'
const scrollKey = horizontal ? 'scrollLeft' : 'scrollTop'

const defaultScrollToFn = React.useCallback(
offset => {
parentRef.current[scrollKey] = offset
},
[parentRef, scrollKey]
)

scrollToFn = scrollToFn || defaultScrollToFn

const { [sizeKey]: outerSize } = useRect(parentRef) || {
[sizeKey]: 0,
}
Expand All @@ -23,12 +34,17 @@ export function useVirtual({
_setScrollOffset(newScrollOffset)
})

const scrollOffsetPlusSize = scrollOffset + outerSize
const scrollOffsetPlusOuterSize = scrollOffset + outerSize

const [measuredCache, setMeasuredCache] = React.useState({})

React.useEffect(() => {
if (estimateSize || size) setMeasuredCache({})
const mountedRef = React.useRef()

useIsomorphicLayoutEffect(() => {
if (mountedRef.current) {
if (estimateSize || size) setMeasuredCache({})
}
mountedRef.current = true
}, [estimateSize, size])

const measurements = React.useMemo(() => {
Expand All @@ -48,7 +64,7 @@ export function useVirtual({
return measurements
}, [estimateSize, measuredCache, size])

const total = measurements[size - 1]?.end || 0
const totalSize = measurements[size - 1]?.end || 0

let start = React.useMemo(
() => measurements.find(rowStat => rowStat.end >= scrollOffset),
Expand All @@ -58,8 +74,8 @@ export function useVirtual({
() =>
[...measurements]
.reverse()
.find(rowStat => rowStat.start <= scrollOffsetPlusSize),
[measurements, scrollOffsetPlusSize]
.find(rowStat => rowStat.start <= scrollOffsetPlusOuterSize),
[measurements, scrollOffsetPlusOuterSize]
)

let startIndex = start ? start.index : 0
Expand Down Expand Up @@ -97,28 +113,88 @@ export function useVirtual({
return virtualItems
}, [startIndex, endIndex, measurements, sizeKey])

const latestRef = React.useRef()
latestRef.current = {
outerSize,
scrollOffset,
scrollOffsetPlusOuterSize,
totalSize,
}

const scrollToOffset = React.useCallback(
offset => {
_setScrollOffset(offset)
parentRef.current[scrollKey] = offset
(offset, { align = 'start' } = {}) => {
const {
outerSize,
scrollOffset,
scrollOffsetPlusOuterSize,
totalSize,
} = latestRef.current

offset = Math.max(0, Math.min(offset, totalSize - outerSize))

if (align === 'auto') {
if (offset <= scrollOffset) {
align = 'start'
} else if (offset >= scrollOffsetPlusOuterSize) {
align = 'end'
} else {
align = 'start'
}
}

if (align === 'start') {
scrollToFn(offset)
} else if (align === 'end') {
scrollToFn(offset - outerSize)
} else if (align === 'center') {
scrollToFn(offset - outerSize / 2)
}
},
[parentRef, scrollKey]
[scrollToFn]
)

const scrollToIndex = React.useCallback(
index => {
const getIndexOffset = React.useCallback(
(index, { align = 'start' } = {}) => {
const measurement = measurements[index]

if (measurement) {
scrollToOffset(measurement.start)
if (!measurement) {
return
}

if (align === 'auto') {
if (measurement.end >= scrollOffsetPlusOuterSize) {
align = 'end'
} else {
align = 'start'
}
}

let offset =
align === 'center'
? measurement.start + measurement.size / 2
: align === 'end'
? measurement.end
: measurement.start

return offset
},
[measurements, scrollOffsetPlusOuterSize]
)

const scrollToIndex = React.useCallback(
(index, options) => {
const offset = getIndexOffset(index, options)
if (typeof offset !== 'undefined') {
scrollToOffset(offset, options)
}
},
[measurements, scrollToOffset]
[getIndexOffset, scrollToOffset]
)

return {
virtualItems,
totalSize: total,
totalSize,
getIndexOffset,
scrollToOffset,
scrollToIndex,
}
Expand Down

0 comments on commit f0e281f

Please sign in to comment.