Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 71 additions & 21 deletions demo/Demo.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { useState } from 'react';
import { useState, type ReactNode } from 'react';
import { StickToBottom, useStickToBottomContext } from '../src/StickToBottom';
import { useFakeMessages } from './useFakeMessages';



function ScrollToBottom() {
const { isAtBottom, scrollToBottom } = useStickToBottomContext();

return (
!isAtBottom && (
<button
className="absolute i-ph-arrow-circle-down-fill text-4xl rounded-lg left-[50%] translate-x-[-50%] bottom-0"
className="fixed i-ph-arrow-circle-down-fill text-4xl rounded-lg left-[50%] translate-x-[-50%] bottom-4"
onClick={() => scrollToBottom()}
/>
)
);
}

function MessagesContent({ messages }: { messages: React.ReactNode[][] }) {
function MessagesContent({ messages }: { messages: ReactNode[][] }) {
const { stopScroll } = useStickToBottomContext();

return (
<>
<div className="relative w-full flex flex-col overflow-hidden">
Expand All @@ -28,14 +28,12 @@ function MessagesContent({ messages }: { messages: React.ReactNode[][] }) {
more testing text...
</Message>
))}

{messages.map((message, i) => (
<Message key={i}>{message}</Message>
))}
</StickToBottom.Content>
<ScrollToBottom />
</div>

<div className="flex justify-center pt-4">
<button className="rounded bg-slate-600 text-white px-4 py-2" onClick={() => stopScroll()}>
Stop Scroll
Expand All @@ -47,11 +45,9 @@ function MessagesContent({ messages }: { messages: React.ReactNode[][] }) {

function Messages({ animation, speed }: { animation: ScrollBehavior; speed: number }) {
const messages = useFakeMessages(speed);

return (
<div className="prose flex flex-col gap-2 w-full overflow-hidden">
<h2 className="flex justify-center">{animation}:</h2>

<StickToBottom
className="h-[50vh] flex flex-col"
resize={animation}
Expand All @@ -63,25 +59,79 @@ function Messages({ animation, speed }: { animation: ScrollBehavior; speed: numb
);
}

// Document scroll demo content
function DocumentScrollDemoContent({ speed }: { speed: number }) {
const messages = useFakeMessages(speed);
const { contentRef } = useStickToBottomContext();
return (
<>

<div ref={contentRef} className="flex flex-col gap-4 p-6">
{[...Array(10)].map((_, i) => (
<Message key={i}>
<h1>This is a test</h1>
more testing text...
</Message>
))}
{messages.map((message, i) => (
<Message key={i}>{message}</Message>
))}
</div>
<ScrollToBottom />

</>
);
}

export function Demo() {
const [speed, setSpeed] = useState(0.2);
const [mode, setMode] = useState<'element' | 'document'>('element');

return (
<div className="flex flex-col gap-10 p-10 items-center w-full">
<input
className="w-full max-w-screen-lg"
type="range"
value={speed}
onChange={(e) => setSpeed(+e.target.value)}
min={0}
max={1}
step={0.01}
></input>
{/* Header with navigation */}
<header className="w-full max-w-screen-lg flex justify-center gap-4 text-lg font-bold">
<button
className={mode === 'element' ? 'underline text-blue-600' : 'text-blue-600 hover:underline'}
onClick={() => setMode('element')}
>
Element Scroll Demo
</button>
<button
className={mode === 'document' ? 'underline text-blue-600' : 'text-blue-600 hover:underline'}
onClick={() => setMode('document')}
>
Document Scroll Demo
</button>
</header>

<div className="flex gap-6 w-full max-w-screen-lg">
<Messages speed={speed} animation="smooth" />
<Messages speed={speed} animation="instant" />
{/* Sticky range selector */}
<div className="sticky top-0 bg-white w-full max-w-screen-lg z-10 py-4">
<input
className="w-full"
type="range"
value={speed}
onChange={(e) => setSpeed(+e.target.value)}
min={0}
max={1}
step={0.01}
/>
</div>

{/* Conditionally render demos */}
{mode === 'element' && (
<div className="flex gap-6 w-full max-w-screen-lg">
<Messages speed={speed} animation="smooth" />
<Messages speed={speed} animation="instant" />
</div>
)}
{mode === 'document' && (
<div className="flex gap-6 w-full max-w-screen-lg">
<StickToBottom scrollMode="document">
<DocumentScrollDemoContent speed={speed} />
</StickToBottom>
</div>
)}
</div>
);
}
Expand Down
36 changes: 29 additions & 7 deletions src/StickToBottom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface StickToBottomContext {
get targetScrollTop(): GetTargetScrollTop | null;
set targetScrollTop(targetScrollTop: GetTargetScrollTop | null);
state: StickToBottomState;
scrollMode: "element" | "document"; // Add scrollMode to context
}

const StickToBottomContext = createContext<StickToBottomContext | null>(null);
Expand All @@ -45,6 +46,7 @@ export interface StickToBottomProps
contextRef?: React.Ref<StickToBottomContext>;
instance?: ReturnType<typeof useStickToBottom>;
children: ((context: StickToBottomContext) => ReactNode) | ReactNode;
scrollMode?: "element" | "document"; // Add scrollMode prop
}

const useIsomorphicLayoutEffect =
Expand All @@ -59,6 +61,7 @@ export function StickToBottom({
damping,
stiffness,
targetScrollTop: currentTargetScrollTop,
scrollMode = "element", // Destructure scrollMode prop with default
contextRef,
...props
}: StickToBottomProps) {
Expand All @@ -79,6 +82,7 @@ export function StickToBottom({
resize,
initial,
targetScrollTop,
scrollMode, // Pass scrollMode to the hook
});

const {
Expand All @@ -89,8 +93,14 @@ export function StickToBottom({
isAtBottom,
escapedFromLock,
state,
// Destructure scrollMode from hook result (though we already have it from props)
// Might be useful if using a passed-in instance
scrollMode: instanceScrollMode,
} = instance ?? defaultInstance;

// Use the scrollMode passed via props primarily, fallback to instance if provided externally
const effectiveScrollMode = instance ? instanceScrollMode : scrollMode;

const context = useMemo<StickToBottomContext>(
() => ({
scrollToBottom,
Expand All @@ -100,6 +110,7 @@ export function StickToBottom({
escapedFromLock,
contentRef,
state,
scrollMode: effectiveScrollMode, // Add scrollMode to context value
get targetScrollTop() {
return customTargetScrollTop.current;
},
Expand All @@ -115,20 +126,21 @@ export function StickToBottom({
stopScroll,
escapedFromLock,
state,
effectiveScrollMode, // Add effectiveScrollMode to dependency array
],
);

useImperativeHandle(contextRef, () => context, [context]);

// Conditionally apply overflow style only in element mode
useIsomorphicLayoutEffect(() => {
if (!scrollRef.current) {
return;
}

if (getComputedStyle(scrollRef.current).overflow === "visible") {
scrollRef.current.style.overflow = "auto";
if (effectiveScrollMode === "element" && scrollRef.current) {
if (getComputedStyle(scrollRef.current).overflow === "visible") {
scrollRef.current.style.overflow = "auto";
}
}
}, []);
// Add effectiveScrollMode to dependency array
}, [effectiveScrollMode]);

return (
<StickToBottomContext.Provider value={context}>
Expand All @@ -148,6 +160,16 @@ export namespace StickToBottom {
export function Content({ children, ...props }: ContentProps) {
const context = useStickToBottomContext();

// In 'document' mode, don't render the outer scroll div
if (context.scrollMode === "document") {
return (
<div {...props} ref={context.contentRef}>
{typeof children === "function" ? children(context) : children}
</div>
);
}

// Default 'element' mode rendering
return (
<div
ref={context.scrollRef}
Expand Down
Loading