Skip to content

Instantly share code, notes, and snippets.

@ephys
Created June 26, 2021 17:34
Show Gist options
  • Save ephys/9f57f214a7a495a3062a24a9addf33df to your computer and use it in GitHub Desktop.
Save ephys/9f57f214a7a495a3062a24a9addf33df to your computer and use it in GitHub Desktop.
Div Overflow Indicator / Detector
.top {
--mask-top: transparent;
}
.right {
--mask-right: transparent;
}
.bottom {
--mask-bottom: transparent;
}
.left {
--mask-left: transparent;
}
.overflowIndicator {
mask-image: linear-gradient(
to top,
black 90%,
var(--mask-top, black) 100%
),
linear-gradient(
to right,
rgb(0 0 0 / 1) 90%,
var(--mask-right, black) 100%
),
linear-gradient(
to bottom,
rgb(0 0 0 / 1) 90%,
var(--mask-bottom, black) 100%
),
linear-gradient(
to left,
rgb(0 0 0 / 1) 90%,
var(--mask-left, black) 100%
);
/* Autoprefixer doesn't generate this one properly */
/* stylelint-disable-next-line */
-webkit-mask-composite: source-in;
mask-composite: intersect;
}
import classes from 'classnames';
import { ReactNode, RefObject, useEffect, useRef, useState } from 'react';
import css from './overflow-indicator.module.scss';
const DEFAULT_OVERFLOW_SIDES = Object.freeze({ left: false, right: false, top: false, bottom: false });
type TOverflowSides = {
/** whether the Element can be scrolled in the left direction */
left: boolean,
/** whether the Element can be scrolled in the right direction */
right: boolean,
/** whether the Element can be scrolled in the top direction */
top: boolean,
/** whether the Element can be scrolled in the bottom direction */
bottom: boolean,
};
/**
* Detects in which direction a given element can be scrolled.
*
* Use cases: displaying an indicator that there is content in a given direction
*
* @param {React.RefObject<any>} manualRef - optional: provide your own RefObject
* @returns {[React.RefObject<any>, TOverflowSides]}
*/
export function useOverflowDetector(manualRef?: RefObject<any>): [RefObject<any>, TOverflowSides] {
const fallbackRef = useRef<any>();
const ref: RefObject<any> = manualRef ?? fallbackRef;
const [overflowSides, setOverflowSides] = useState<TOverflowSides>(DEFAULT_OVERFLOW_SIDES);
useEffect(() => {
const child = ref.current;
if (!child) {
return;
}
function checkScroll() {
const top = child.scrollTop > 0;
const bottom = child.scrollTop < (child.scrollHeight - child.clientHeight);
const left = child.scrollLeft > 0;
const right = child.scrollLeft < (child.scrollWidth - child.clientWidth);
setOverflowSides(old => {
if (old.top === top && old.bottom === bottom && left === old.left && right === old.right) {
return old;
}
return { top, bottom, right, left };
});
}
checkScroll();
let timeout;
let ro: ResizeObserver;
// sometimes the browser hasn't computed the layout before this function runs
// so run it again
if (typeof ResizeObserver !== 'undefined') {
ro = new ResizeObserver(() => {
checkScroll();
});
ro.observe(child);
} else {
timeout = setTimeout(checkScroll, 1000);
}
child.addEventListener('scroll', checkScroll, { passive: true });
// eslint-disable-next-line consistent-return
return () => {
if (ro) {
ro.disconnect();
}
clearTimeout(timeout);
child.removeEventListener('scroll', checkScroll, {});
};
}, []);
return [ref, overflowSides];
}
type TOverflowIndicatorProps = {
targetRef: RefObject<any>,
children: ReactNode,
} | {
targetRef?: undefined | null,
children: (ref: RefObject<any>) => ReactNode,
};
/**
* This component fades to transparent sides that have overflowing content.
* Useful to indicate to the user that there is more content in this direction.
*
* Either provide {targetRef} as a prop, or receive it from this component by passing a function as the child component.
*
* @constructor
*/
export function OverflowIndicator(props: TOverflowIndicatorProps) {
const { targetRef, children } = props;
const [ref, sides] = useOverflowDetector(targetRef);
return (
<div
className={classes(css.overflowIndicator, {
[css.top]: sides.top,
[css.bottom]: sides.bottom,
[css.right]: sides.right,
[css.left]: sides.left,
})}
>
{typeof children === 'function' ? children(ref) : children}
</div>
);
}
@ephys
Copy link
Author

ephys commented Jun 26, 2021

OverflowIndicator Demo

Here we have the overflow indicator fading the element to white on either horizontal side as there is more content to see

image

Note: the background color doesn't matter as it doesn't fade to white, it fades to transparent (thanks, clip!)

image

The same thing, but vertically

image

And again, but only one side is fading

image

useOverflowDetector demo

Another use case, other than OverflowIndicator:

Here we display the scroll button only if there is content in that direction

image
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment