/*!--------------------------------------------------------------------------------------------- * Copyright (c) StackBlitz. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as React from "react"; import { type ReactNode, createContext, useContext, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, } from "react"; import { type GetTargetScrollTop, type ScrollToBottom, type StickToBottomInstance, type StickToBottomOptions, type StickToBottomState, type StopScroll, useStickToBottom, } from "./useStickToBottom"; export interface StickToBottomContext { contentRef: React.MutableRefObject & React.RefCallback; scrollRef: React.MutableRefObject & React.RefCallback; scrollToBottom: ScrollToBottom; stopScroll: StopScroll; isAtBottom: boolean; escapedFromLock: boolean; get targetScrollTop(): GetTargetScrollTop | null; set targetScrollTop(targetScrollTop: GetTargetScrollTop | null); state: StickToBottomState; } const StickToBottomContext = createContext(null); export interface StickToBottomProps extends Omit, "children">, StickToBottomOptions { contextRef?: React.Ref; instance?: StickToBottomInstance; children: ((context: StickToBottomContext) => ReactNode) | ReactNode; enabled?: boolean; } const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect; export function StickToBottom({ instance, children, resize, initial, mass, damping, stiffness, targetScrollTop: currentTargetScrollTop, contextRef, ...props }: StickToBottomProps): ReactNode { const customTargetScrollTop = useRef(null); const targetScrollTop = React.useCallback( (target, elements) => { const get = context?.targetScrollTop ?? currentTargetScrollTop; return get?.(target, elements) ?? target; }, [currentTargetScrollTop], ); const { enabled = true } = props; const defaultInstance = useStickToBottom({ enabled, mass, damping, stiffness, resize, initial, targetScrollTop, }); const { scrollRef, contentRef, scrollToBottom, stopScroll, isAtBottom, escapedFromLock, state, } = instance ?? defaultInstance; const context = useMemo( () => ({ scrollToBottom, stopScroll, scrollRef, isAtBottom, escapedFromLock, contentRef, state, get targetScrollTop() { return customTargetScrollTop.current; }, set targetScrollTop(targetScrollTop: GetTargetScrollTop | null) { customTargetScrollTop.current = targetScrollTop; }, }), [ scrollToBottom, isAtBottom, contentRef, scrollRef, stopScroll, escapedFromLock, state, ], ); useImperativeHandle(contextRef, () => context, [context]); useIsomorphicLayoutEffect(() => { if (!scrollRef.current) { return; } if (getComputedStyle(scrollRef.current).overflow === "visible") { scrollRef.current.style.overflow = "auto"; } }, []); return (
{typeof children === "function" ? children(context) : children}
); } export namespace StickToBottom { export interface ContentProps extends Omit, "children"> { children: ((context: StickToBottomContext) => ReactNode) | ReactNode; scrollClassName?: string; } export function Content({ children, scrollClassName, ...props }: ContentProps): ReactNode { const context = useStickToBottomContext(); return (
{typeof children === "function" ? children(context) : children}
); } } /** * Use this hook inside a component to gain access to whether the component is at the bottom of the scrollable area. */ export function useStickToBottomContext(): StickToBottomContext { const context = useContext(StickToBottomContext); if (!context) { throw new Error( "use-stick-to-bottom component context must be used within a StickToBottom component", ); } return context; }