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
42 changes: 26 additions & 16 deletions demo/Demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,21 @@ function ScrollToBottom() {
return (
!isAtBottom && (
<button
className="absolute i-ph-arrow-circle-down-fill text-4xl rounded-lg left-[50%] translate-x-[-50%] bottom-0"
className='absolute i-ph-arrow-circle-down-fill text-4xl rounded-lg left-[50%] translate-x-[-50%] bottom-0'
onClick={() => scrollToBottom()}
/>
)
);
}

function MessagesContent({ messages }: { messages: React.ReactNode[][] }) {
const { stopScroll } = useStickToBottomContext();
const { stopScroll, disableAutoScroll, enableAutoScroll } = useStickToBottomContext();
const [disabled, setDisabled] = useState(false);

return (
<>
<div className="relative w-full flex flex-col overflow-hidden">
<StickToBottom.Content className="flex flex-col gap-4 p-6">
<div className='relative w-full flex flex-col overflow-hidden'>
<StickToBottom.Content className='flex flex-col gap-4 p-6'>
{[...Array(10)].map((_, i) => (
<Message key={i}>
<h1>This is a test</h1>
Expand All @@ -36,10 +37,19 @@ function MessagesContent({ messages }: { messages: React.ReactNode[][] }) {
<ScrollToBottom />
</div>

<div className="flex justify-center pt-4">
<button className="rounded bg-slate-600 text-white px-4 py-2" onClick={() => stopScroll()}>
<div className='flex justify-center pt-4 gap-4'>
<button className='rounded bg-slate-600 text-white px-4 py-2' onClick={() => stopScroll()}>
Stop Scroll
</button>
<button
className='rounded bg-slate-600 text-white px-4 py-2'
onClick={() => {
setDisabled((prev) => !prev);
disabled ? enableAutoScroll() : disableAutoScroll();
}}
>
{disabled ? 'Enable' : 'Disable'} Auto Scroll
</button>
</div>
</>
);
Expand All @@ -49,11 +59,11 @@ function Messages({ animation, speed }: { animation: ScrollBehavior; speed: numb
const messages = useFakeMessages(speed);

return (
<div className="prose flex flex-col gap-2 w-full overflow-hidden">
<h2 className="flex justify-center">{animation}:</h2>
<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"
className='h-[50vh] flex flex-col'
resize={animation}
initial={animation === 'instant' ? 'instant' : { mass: 10 }}
>
Expand All @@ -67,25 +77,25 @@ export function Demo() {
const [speed, setSpeed] = useState(0.2);

return (
<div className="flex flex-col gap-10 p-10 items-center w-full">
<div className='flex flex-col gap-10 p-10 items-center w-full'>
<input
className="w-full max-w-screen-lg"
type="range"
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>

<div className="flex gap-6 w-full max-w-screen-lg">
<Messages speed={speed} animation="smooth" />
<Messages speed={speed} animation="instant" />
<div className='flex gap-6 w-full max-w-screen-lg'>
<Messages speed={speed} animation='smooth' />
<Messages speed={speed} animation='instant' />
</div>
</div>
);
}

function Message({ children }: { children: React.ReactNode }) {
return <div className="bg-gray-100 rounded-lg p-4 shadow-md break-words">{children}</div>;
return <div className='bg-gray-100 rounded-lg p-4 shadow-md break-words'>{children}</div>;
}
10 changes: 10 additions & 0 deletions src/StickToBottom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
type StickToBottomOptions,
type StickToBottomState,
type StopScroll,
type DisableAutoScroll,
type EnableAutoScroll,
useStickToBottom,
} from "./useStickToBottom.js";

Expand All @@ -31,6 +33,8 @@ export interface StickToBottomContext {
React.RefCallback<HTMLElement>;
scrollToBottom: ScrollToBottom;
stopScroll: StopScroll;
disableAutoScroll: DisableAutoScroll;
enableAutoScroll: EnableAutoScroll;
isAtBottom: boolean;
escapedFromLock: boolean;
get targetScrollTop(): GetTargetScrollTop | null;
Expand Down Expand Up @@ -87,6 +91,8 @@ export function StickToBottom({
contentRef,
scrollToBottom,
stopScroll,
disableAutoScroll,
enableAutoScroll,
isAtBottom,
escapedFromLock,
state,
Expand All @@ -96,6 +102,8 @@ export function StickToBottom({
() => ({
scrollToBottom,
stopScroll,
disableAutoScroll,
enableAutoScroll,
scrollRef,
isAtBottom,
escapedFromLock,
Expand All @@ -114,6 +122,8 @@ export function StickToBottom({
contentRef,
scrollRef,
stopScroll,
disableAutoScroll,
enableAutoScroll,
escapedFromLock,
state,
],
Expand Down
35 changes: 30 additions & 5 deletions src/useStickToBottom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ export type ScrollToBottom = (
scrollOptions?: ScrollToBottomOptions,
) => Promise<boolean> | boolean;
export type StopScroll = () => void;
export type DisableAutoScroll = () => void;
export type EnableAutoScroll = () => void;

const STICK_TO_BOTTOM_OFFSET_PX = 70;
const SIXTY_FPS_INTERVAL_MS = 1000 / 60;
Expand All @@ -152,7 +154,7 @@ export const useStickToBottom = (
const [escapedFromLock, updateEscapedFromLock] = useState(false);
const [isAtBottom, updateIsAtBottom] = useState(options.initial !== false);
const [isNearBottom, setIsNearBottom] = useState(false);

const disableAutoScrollRef = useRef(false);
const optionsRef = useRef<StickToBottomOptions>(null!);
optionsRef.current = options;

Expand Down Expand Up @@ -353,7 +355,7 @@ export const useStickToBottom = (
* requested animatino.
*/
if (state.scrollTop < state.calculatedTargetScrollTop) {
return scrollToBottom({
return internalScrollToBottom({
animation: mergeAnimations(
optionsRef.current,
optionsRef.current.resize,
Expand Down Expand Up @@ -391,11 +393,31 @@ export const useStickToBottom = (
[setIsAtBottom, isSelecting, state],
);

const stopScroll = useCallback((): void => {
const internalScrollToBottom = useCallback<ScrollToBottom>(
(scrollOptions) => {
if (disableAutoScrollRef.current) return Promise.resolve(false);
return scrollToBottom(scrollOptions);
},
[scrollToBottom],
);

const stopScroll = useCallback<StopScroll>(() => {
setEscapedFromLock(true);
setIsAtBottom(false);
}, [setEscapedFromLock, setIsAtBottom]);

const disableAutoScroll = useCallback<DisableAutoScroll>(() => {
setEscapedFromLock(true);
setIsAtBottom(false);
disableAutoScrollRef.current = true;
}, [setEscapedFromLock, setIsAtBottom]);

const enableAutoScroll = useCallback<EnableAutoScroll>(() => {
setEscapedFromLock(true);
setIsAtBottom(false);
disableAutoScrollRef.current = false;
}, [setEscapedFromLock, setIsAtBottom]);

const handleScroll = useCallback(
({ target }: Event) => {
if (target !== scrollRef.current) {
Expand Down Expand Up @@ -538,8 +560,7 @@ export const useStickToBottom = (
? optionsRef.current.resize
: optionsRef.current.initial,
);

scrollToBottom({
internalScrollToBottom({
animation,
wait: true,
preserveScrollPosition: true,
Expand Down Expand Up @@ -584,6 +605,8 @@ export const useStickToBottom = (
scrollRef,
scrollToBottom,
stopScroll,
disableAutoScroll,
enableAutoScroll,
isAtBottom: isAtBottom || isNearBottom,
isNearBottom,
escapedFromLock,
Expand All @@ -598,6 +621,8 @@ export interface StickToBottomInstance {
React.RefCallback<HTMLElement>;
scrollToBottom: ScrollToBottom;
stopScroll: StopScroll;
disableAutoScroll: DisableAutoScroll;
enableAutoScroll: EnableAutoScroll;
isAtBottom: boolean;
isNearBottom: boolean;
escapedFromLock: boolean;
Expand Down