Created
June 26, 2021 17:34
-
-
Save ephys/9f57f214a7a495a3062a24a9addf33df to your computer and use it in GitHub Desktop.
Div Overflow Indicator / Detector
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
OverflowIndicator Demo
Here we have the overflow indicator fading the element to white on either horizontal side as there is more content to see
Note: the background color doesn't matter as it doesn't fade to white, it fades to transparent (thanks,
clip
!)The same thing, but vertically
And again, but only one side is fading
useOverflowDetector demo
Another use case, other than OverflowIndicator:
Here we display the scroll button only if there is content in that direction