nx/nx-dev/ui-scrollable-content/src/lib/scrollable-content.tsx
Jack Hsu 9dca7c7025
docs(core): add scroll_25, scroll_50, scroll_75, and scroll_90 events to track engagement (#27461)
This PR adds events to track engagement in our docs. Since we use a
scrollable `<div>` in our docs, the normal `scroll` events in GA do not
work.

A new `<ScrollableContent>` component is added that will do two things:
- Send `scroll_25`, `scroll_50`, `scroll_75`, and `scroll_90` events
whenever the user scrolls to 25%, 50%, 75%, of 90% of the content
- Optionally reset scroll top to zero whenever router changes (existing
behavior)

All of the places where we have content in a scrollable `<div>` is
replaced with `<ScrollableContent>`.

Note: 90% means user has reached the bottom, since it's not usually
possible to get to 100%.
2024-08-16 12:25:20 -04:00

85 lines
2.5 KiB
TypeScript

import type { JSX, ReactNode, UIEvent } from 'react';
import { useEffect, useRef } from 'react';
import { useRouter } from 'next/router';
import { sendCustomEvent } from '@nx/nx-dev/feature-analytics';
interface ScrollViewProps {
children?: ReactNode;
resetScrollOnNavigation?: boolean;
}
// Takes in a percentage like 0.33 and rounds to the nearest 0, 25%, 50%, 75%, 90% bucket.
function getScrollDepth(pct: number): 0 | 25 | 50 | 75 | 90 {
// Anything greater than 0.9 is just 90% and counts as reaching the bottom.
if (pct >= 0.9) {
return 90;
}
// Otherwise, divide into quarters (0, 25, 50, 75).
if (pct < 0.25) return 0;
if (pct < 0.5) return 25;
if (pct < 0.75) return 50;
return 75;
}
export function ScrollableContent(props: ScrollViewProps): JSX.Element {
const wrapperElement = useRef<HTMLDivElement | null>(null);
const router = useRouter();
const scrollDepth = useRef(0);
const shouldTrackScroll = useRef(true);
useEffect(() => {
if (!props.resetScrollOnNavigation) {
scrollDepth.current = 0;
return;
}
shouldTrackScroll.current = false;
setTimeout(() => {
scrollDepth.current = 0;
shouldTrackScroll.current = true;
}, 1000);
const handleRouteChange = (url: string) => {
if (url.includes('#')) return;
if (!wrapperElement.current) return;
wrapperElement.current.scrollTo({
top: 0,
left: 0,
behavior: 'smooth',
});
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => router.events.off('routeChangeComplete', handleRouteChange);
}, [props.resetScrollOnNavigation, router, wrapperElement]);
const handleScroll = (evt: UIEvent<HTMLDivElement>) => {
if (!shouldTrackScroll.current) return;
const el = evt.currentTarget;
const { scrollHeight, scrollTop, offsetHeight } = el;
const depth = getScrollDepth((scrollTop + offsetHeight) / scrollHeight);
// Only track changes that are greater than the previous scroll depth.
// If a user already viewed 90% of the page we don't need to know they went back to 50%.
if (depth > scrollDepth.current) {
scrollDepth.current = depth;
sendCustomEvent(`scroll_${depth}`, 'scroll', router.asPath);
}
};
return (
<div
ref={wrapperElement}
id="wrapper"
data-testid="wrapper"
className="relative flex flex-grow flex-col items-stretch justify-start overflow-y-scroll"
onScroll={handleScroll}
>
{props.children}
</div>
);
}
export default ScrollableContent;