Skip to content

Commit cba9873

Browse files
committed
Add refs argument and useOverflow hook
1 parent b97b8e2 commit cba9873

File tree

5 files changed

+133
-57
lines changed

5 files changed

+133
-57
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
!.*rc.js
2+
dist

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
!.*rc.js
2+
dist

README.md

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@ indicators if you’d like to overlay them on the scrollable viewport.
147147
<td valign="top" align="right" rowspan="1"></td>
148148
<td valign="top" valign="top" rowspan="1">
149149

150-
Callback that receives the latest overflow state, if you’d like to react to
151-
scrollability in a custom way.
150+
Callback that receives the latest overflow state and an object of refs, if you’d
151+
like to react to overflow in a custom way.
152152

153153
</td>
154154
</tr>
@@ -272,8 +272,9 @@ One&nbsp;of… <br>
272272
<td valign="top" valign="top" rowspan="1">
273273

274274
Indicator to render when scrolling is allowed in the requested direction. If
275-
given a function, it will be passed the overflow state and its result will be
276-
rendered.
275+
given a function, it will be passed the overflow state and and object containing
276+
the `viewport` ref (you can use the `refs` parameter to render an indicator that
277+
is also a button that scrolls the viewport).
277278

278279
</td>
279280
</tr>
@@ -298,6 +299,55 @@ active when scrolling is allowed in any direction.
298299
</table>
299300
<!-- AUTO-GENERATED-CONTENT:END -->
300301

302+
### useOverflow
303+
304+
This hook provides full access to the Overflow’s context containing its current
305+
`state` and `refs`. While `<Overflow.Indicator>` should be good enough for most
306+
use cases, you can use this if you have other use cases in mind. Must be used
307+
inside an `<Overflow>` ancestor.
308+
309+
Returns an object like:
310+
311+
```js
312+
{
313+
state: {
314+
canScroll: {
315+
up: Boolean,
316+
left: Boolean,
317+
right: Boolean,
318+
down: Boolean
319+
}
320+
},
321+
dispatch: Function,
322+
tolerance: Number | String,
323+
refs: {
324+
viewport: Object
325+
}
326+
}
327+
```
328+
329+
## Examples
330+
331+
### Make the indicator a button that scrolls the viewport
332+
333+
```jsx
334+
<Overflow.Indicator direction="down">
335+
{(canScroll, refs) => (
336+
<button
337+
onClick={() => {
338+
refs.viewport.current.scrollBy({
339+
top: refs.viewport.current.clientHeight,
340+
behavior: 'smooth'
341+
});
342+
}}
343+
style={{ position: 'absolute', right: 10, bottom: 10 }}
344+
>
345+
{canScroll ? '' : ''}
346+
</button>
347+
)}
348+
</Overflow.Indicator>
349+
```
350+
301351
## Implementation Details
302352

303353
Instead of the traditional method of listening for `scroll` and `resize` events,

pages/index.js

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -372,29 +372,37 @@ export default function DemoPage() {
372372
</ul>
373373
</Overflow.Content>
374374
<Overflow.Indicator direction="down">
375-
{canScroll => (
376-
<span
375+
{(canScroll, refs) => (
376+
<button
377+
onClick={() => {
378+
console.log(refs.viewport.current);
379+
refs.viewport.current.scrollBy({
380+
top: refs.viewport.current.clientHeight,
381+
behavior: 'smooth'
382+
});
383+
}}
377384
style={{
385+
display: 'inline-block',
378386
position: 'absolute',
379387
right: 0,
380388
bottom: 12,
381-
transform: 'translate3d(-50%, 0, 0)',
382-
display: 'inline-block',
383389
width: 40,
384390
height: 40,
385-
fontSize: 24,
391+
transform: 'translate3d(-50%, 0, 0)',
386392
border: '1px solid #ddd',
387-
lineHeight: '40px',
388-
background: 'white',
389393
borderRadius: '50%',
394+
padding: 0,
395+
fontSize: 24,
396+
lineHeight: '40px',
390397
textAlign: 'center',
398+
background: 'white',
391399
opacity: canScroll ? 1 : 0,
392400
animation: 'bounce 2s infinite ease',
393401
transition: 'opacity 500ms 500ms ease-out'
394402
}}
395403
>
396404
{canScroll ? '⏬' : '✅'}
397-
</span>
405+
</button>
398406
)}
399407
</Overflow.Indicator>
400408
</Overflow>

src/index.js

Lines changed: 61 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import PropTypes from 'prop-types';
99

1010
const Context = React.createContext();
1111

12+
export function useOverflow() {
13+
return useContext(Context);
14+
}
15+
1216
const containerStyle = {
1317
display: 'flex',
1418
flexDirection: 'column',
@@ -60,8 +64,6 @@ function getInitialState() {
6064
};
6165
}
6266

63-
const emptyStyle = {};
64-
6567
/**
6668
* The overflow state provider. At a minimum it must contain an
6769
* `<Overflow.Content>` element, otherwise it will do nothing.
@@ -99,12 +101,13 @@ const emptyStyle = {};
99101
export default function Overflow({
100102
children,
101103
onStateChange,
104+
style: styleProp,
102105
tolerance = 0,
103106
...rest
104107
}) {
105108
const [state, dispatch] = useReducer(reducer, null, getInitialState);
106109
const hidden = rest.hidden;
107-
const styleProp = rest.style || emptyStyle;
110+
const viewportRef = useRef();
108111

109112
const style = useMemo(
110113
() => ({
@@ -116,27 +119,33 @@ export default function Overflow({
116119
// `display: none` and allow that, otherwise ensure we use the value from
117120
// `containerStyle`.
118121
display:
119-
hidden || styleProp.display === 'none' ? 'none' : containerStyle.display
122+
hidden || (styleProp && styleProp.display === 'none')
123+
? 'none'
124+
: containerStyle.display
120125
}),
121126
[hidden, styleProp]
122127
);
123128

124-
const context = useMemo(() => {
125-
return {
129+
const refs = useMemo(() => ({ viewport: viewportRef }), []);
130+
131+
const context = useMemo(
132+
() => ({
126133
state,
127134
dispatch,
128-
tolerance
129-
};
130-
}, [state, tolerance]);
135+
tolerance,
136+
refs
137+
}),
138+
[refs, state, tolerance]
139+
);
131140

132141
useEffect(() => {
133142
if (onStateChange) {
134-
onStateChange(state);
143+
onStateChange(state, refs);
135144
}
136-
}, [onStateChange, state]);
145+
}, [onStateChange, refs, state]);
137146

138147
return (
139-
<div data-overflow-wrapper="" {...rest} style={style}>
148+
<div data-overflow-wrapper="" style={style} {...rest}>
140149
<Context.Provider value={context}>{children}</Context.Provider>
141150
</div>
142151
);
@@ -150,8 +159,8 @@ Overflow.propTypes = {
150159
*/
151160
children: PropTypes.node,
152161
/**
153-
* Callback that receives the latest overflow state, if you’d like to react
154-
* to scrollability in a custom way.
162+
* Callback that receives the latest overflow state and an object of refs, if
163+
* you’d like to react to overflow in a custom way.
155164
*/
156165
onStateChange: PropTypes.func,
157166
/**
@@ -176,8 +185,8 @@ Overflow.propTypes = {
176185
* interfering with the styles this component needs to function.
177186
*/
178187
function OverflowContent({ children, style: styleProp, ...rest }) {
179-
const { dispatch, tolerance } = useContext(Context);
180-
const rootRef = useRef();
188+
const { dispatch, tolerance, refs } = useOverflow();
189+
const { viewport: viewportRef } = refs;
181190
const contentRef = useRef();
182191
const toleranceRef = useRef();
183192
const watchRef = tolerance ? toleranceRef : contentRef;
@@ -186,7 +195,7 @@ function OverflowContent({ children, style: styleProp, ...rest }) {
186195
useEffect(() => {
187196
let ignore = false;
188197

189-
const root = rootRef.current;
198+
const root = viewportRef.current;
190199

191200
const createObserver = (direction, rootMargin) => {
192201
const threshold = 1e-12;
@@ -228,7 +237,7 @@ function OverflowContent({ children, style: styleProp, ...rest }) {
228237
observers.right.disconnect();
229238
observers.down.disconnect();
230239
};
231-
}, [dispatch]);
240+
}, [dispatch, viewportRef]);
232241

233242
useEffect(() => {
234243
const observers = observersRef.current;
@@ -254,25 +263,31 @@ function OverflowContent({ children, style: styleProp, ...rest }) {
254263
};
255264
}, [styleProp]);
256265

266+
const toleranceElement = useMemo(
267+
() =>
268+
tolerance ? (
269+
<div
270+
data-overflow-tolerance
271+
ref={toleranceRef}
272+
style={{
273+
position: 'absolute',
274+
top: tolerance,
275+
left: tolerance,
276+
right: tolerance,
277+
bottom: tolerance,
278+
background: 'transparent',
279+
pointerEvents: 'none',
280+
zIndex: -1
281+
}}
282+
/>
283+
) : null,
284+
[tolerance]
285+
);
286+
257287
return (
258-
<div ref={rootRef} data-overflow-viewport="" style={viewportStyle}>
288+
<div ref={viewportRef} data-overflow-viewport="" style={viewportStyle}>
259289
<div ref={contentRef} data-overflow-content="" style={style} {...rest}>
260-
{tolerance ? (
261-
<div
262-
data-overflow-tolerance
263-
ref={toleranceRef}
264-
style={{
265-
position: 'absolute',
266-
top: tolerance,
267-
left: tolerance,
268-
right: tolerance,
269-
bottom: tolerance,
270-
background: 'transparent',
271-
pointerEvents: 'none',
272-
zIndex: -1
273-
}}
274-
/>
275-
) : null}
290+
{toleranceElement}
276291
{children}
277292
</div>
278293
</div>
@@ -330,18 +345,18 @@ OverflowContent.propTypes = {
330345
* ```
331346
*/
332347
function OverflowIndicator({ children, direction }) {
333-
const context = useContext(Context);
334-
const { canScroll: state } = context.state;
335-
const canScroll = direction
336-
? state[direction]
337-
: state.up || state.left || state.right || state.down;
348+
const { state, refs } = useOverflow();
349+
const { canScroll } = state;
350+
const isActive = direction
351+
? canScroll[direction]
352+
: canScroll.up || canScroll.left || canScroll.right || canScroll.down;
338353

339-
let shouldRender = canScroll;
354+
let shouldRender = isActive;
340355

341356
if (typeof children === 'function') {
342-
const arg = direction ? canScroll : state;
343357
shouldRender = true;
344-
children = children(arg);
358+
const stateArg = direction ? isActive : canScroll;
359+
children = children(stateArg, refs);
345360
}
346361

347362
return shouldRender ? <>{children}</> : null;
@@ -352,8 +367,9 @@ OverflowIndicator.displayName = 'Overflow.Indicator';
352367
OverflowIndicator.propTypes = {
353368
/**
354369
* Indicator to render when scrolling is allowed in the requested direction.
355-
* If given a function, it will be passed the overflow state and its result
356-
* will be rendered.
370+
* If given a function, it will be passed the overflow state and and object
371+
* containing the `viewport` ref (you can use the `refs` parameter to render
372+
* an indicator that is also a button that scrolls the viewport).
357373
*/
358374
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
359375
/**

0 commit comments

Comments
 (0)