Line data Source code
1 : import * as React from 'react';
2 :
3 : import { classNames } from '../../utils/classnames.js';
4 : import * as styles from './vertical_scroll_shadows.module.css';
5 :
6 1 : const SCROLL_EDGE_TOLERANCE = 1;
7 :
8 : export interface VerticalScrollOverflow {
9 : top: boolean;
10 : bottom: boolean;
11 : }
12 :
13 1 : export function getVerticalScrollOverflow(element: Pick<HTMLElement, 'clientHeight' | 'scrollHeight' | 'scrollTop'>): VerticalScrollOverflow {
14 1 : const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
15 1 : return {
16 1 : top: element.scrollTop > SCROLL_EDGE_TOLERANCE,
17 1 : bottom: maxScrollTop - element.scrollTop > SCROLL_EDGE_TOLERANCE,
18 1 : };
19 1 : }
20 :
21 : interface VerticalScrollShadowsProps {
22 : getScrollElement: () => HTMLElement | null;
23 : prominent?: boolean;
24 : refreshKey?: unknown;
25 : rightInset?: number;
26 : }
27 :
28 1 : const NO_OVERFLOW: VerticalScrollOverflow = { top: false, bottom: false };
29 :
30 1 : export const VerticalScrollShadows: React.FC<VerticalScrollShadowsProps> = ({
31 1 : getScrollElement,
32 1 : prominent = false,
33 1 : refreshKey,
34 1 : rightInset = 0,
35 1 : }) => {
36 1 : const [overflow, setOverflow] = React.useState(NO_OVERFLOW);
37 1 : const updateOverflow = React.useCallback(() => {
38 1 : const element = getScrollElement();
39 1 : const next = element == null ? NO_OVERFLOW : getVerticalScrollOverflow(element);
40 1 : setOverflow(current => current.top === next.top && current.bottom === next.bottom ? current : next);
41 1 : }, [getScrollElement]);
42 :
43 1 : React.useLayoutEffect(() => {
44 1 : const element = getScrollElement();
45 1 : updateOverflow();
46 1 : if (element == null) return;
47 :
48 1 : element.addEventListener('scroll', updateOverflow, { passive: true });
49 1 : const resizeObserver = new ResizeObserver(updateOverflow);
50 1 : resizeObserver.observe(element);
51 1 : return () => {
52 1 : element.removeEventListener('scroll', updateOverflow);
53 1 : resizeObserver.disconnect();
54 1 : };
55 1 : }, [getScrollElement, refreshKey, updateOverflow]);
56 :
57 1 : const insetStyle = rightInset === 0 ? undefined : { right: rightInset };
58 1 : return (
59 1 : <>
60 1 : <div
61 1 : className={classNames(styles.shadow, styles.top, prominent && styles.prominent)}
62 1 : style={insetStyle}
63 1 : data-scroll-shadow="top"
64 1 : data-visible={overflow.top}
65 1 : aria-hidden="true"
66 1 : />
67 1 : <div
68 1 : className={classNames(styles.shadow, styles.bottom, prominent && styles.prominent)}
69 1 : style={insetStyle}
70 1 : data-scroll-shadow="bottom"
71 1 : data-visible={overflow.bottom}
72 1 : aria-hidden="true"
73 1 : />
74 1 : </>
75 : );
76 1 : };
|