44 lines
1.3 KiB
TypeScript
44 lines
1.3 KiB
TypeScript
// Pin-to-bottom controller for the document scroll.
|
|
//
|
|
// Tracks whether the user is "near" the bottom of the page. While pinned, new
|
|
// content (e.g. streaming tokens) scrolls the viewport to keep up. Once the
|
|
// user scrolls up, pinning releases and updates stop forcing scroll until the
|
|
// user returns to the bottom.
|
|
|
|
const PIN_THRESHOLD_PX = 80;
|
|
|
|
export interface AutoScroll {
|
|
isPinned(): boolean;
|
|
scrollToBottom(behavior?: ScrollBehavior): void;
|
|
maybeScrollToBottom(): void;
|
|
}
|
|
|
|
export function createAutoScroll(): AutoScroll {
|
|
let pinned = true;
|
|
|
|
const scrollEl = () => document.scrollingElement || document.documentElement;
|
|
const distanceFromBottom = () => {
|
|
const el = scrollEl();
|
|
return el.scrollHeight - el.scrollTop - el.clientHeight;
|
|
};
|
|
|
|
const onScroll = () => {
|
|
pinned = distanceFromBottom() < PIN_THRESHOLD_PX;
|
|
};
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
|
|
return {
|
|
isPinned: () => pinned,
|
|
scrollToBottom(behavior: ScrollBehavior = 'auto') {
|
|
window.scrollTo({ top: scrollEl().scrollHeight, behavior });
|
|
pinned = true;
|
|
},
|
|
maybeScrollToBottom() {
|
|
if (!pinned) return;
|
|
requestAnimationFrame(() => {
|
|
window.scrollTo({ top: scrollEl().scrollHeight });
|
|
});
|
|
},
|
|
};
|
|
}
|