add auto_scroll

This commit is contained in:
Khaleel Al-Adhami 2025-02-10 11:12:32 -08:00
parent 8b2c7291d3
commit 7b9c584b5b
3 changed files with 112 additions and 0 deletions

View File

@ -48,6 +48,7 @@ _SUBMOD_ATTRS: dict[str, list[str]] = {
"get_upload_url", "get_upload_url",
"selected_files", "selected_files",
], ],
"auto_scroll": ["auto_scroll"],
} }
__getattr__, __dir__, __all__ = lazy_loader.attach( __getattr__, __dir__, __all__ = lazy_loader.attach(

View File

@ -4,6 +4,7 @@
# ------------------------------------------------------ # ------------------------------------------------------
from . import layout as layout from . import layout as layout
from .auto_scroll import auto_scroll as auto_scroll
from .banner import ConnectionBanner as ConnectionBanner from .banner import ConnectionBanner as ConnectionBanner
from .banner import ConnectionModal as ConnectionModal from .banner import ConnectionModal as ConnectionModal
from .banner import ConnectionPulser as ConnectionPulser from .banner import ConnectionPulser as ConnectionPulser

View File

@ -0,0 +1,110 @@
"""A component that automatically scrolls to the bottom when new content is added."""
from __future__ import annotations
from reflex.components.el.elements.typography import Div
from reflex.constants.compiler import MemoizationDisposition, MemoizationMode
from reflex.utils.imports import ImportDict
from reflex.vars.base import Var
class AutoScroll(Div):
"""A div that automatically scrolls to the bottom when new content is added."""
_memoization_mode = MemoizationMode(disposition=MemoizationDisposition.ALWAYS)
@classmethod
def create(cls, *children, **props):
"""Create an AutoScroll component.
Args:
*children: The children of the component.
**props: The props of the component.
Returns:
An AutoScroll component.
"""
props.setdefault("overflow", "auto")
custom_attrs = props.pop("custom_attrs", {})
custom_attrs["ref"] = Var("containerRef")
return super().create(*children, **props, custom_attrs=custom_attrs)
def add_imports(self) -> ImportDict | list[ImportDict]:
"""Add imports required for the component.
Returns:
The imports required for the component.
"""
return {"react": ["useEffect", "useRef"]}
def add_hooks(self) -> list[str | Var]:
"""Add hooks required for the component.
Returns:
The hooks required for the component.
"""
return [
"const containerRef = useRef(null);",
"const wasNearBottom = useRef(false);",
"const hadScrollbar = useRef(false);",
"""
const checkIfNearBottom = () => {
if (!containerRef.current) return;
const container = containerRef.current;
const nearBottomThreshold = 50; // pixels from bottom to trigger auto-scroll
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
wasNearBottom.current = distanceFromBottom <= nearBottomThreshold;
// Track if container had a scrollbar
hadScrollbar.current = container.scrollHeight > container.clientHeight;
};
""",
"""
const scrollToBottomIfNeeded = () => {
if (!containerRef.current) return;
const container = containerRef.current;
const hasScrollbarNow = container.scrollHeight > container.clientHeight;
// Scroll if:
// 1. User was near bottom, OR
// 2. Container didn't have scrollbar before but does now
if (wasNearBottom.current || (!hadScrollbar.current && hasScrollbarNow)) {
container.scrollTop = container.scrollHeight;
}
// Update scrollbar state for next check
hadScrollbar.current = hasScrollbarNow;};
""",
"""
useEffect(() => {
const container = containerRef.current;
if (!container) return;
// Create ResizeObserver to detect height changes
const resizeObserver = new ResizeObserver(() => {
scrollToBottomIfNeeded();
});
// Track scroll position before height changes
container.addEventListener('scroll', checkIfNearBottom);
// Initial check
checkIfNearBottom();
// Observe container for size changes
resizeObserver.observe(container);
return () => {
container.removeEventListener('scroll', checkIfNearBottom);
resizeObserver.disconnect();
};
});
""",
]
auto_scroll = AutoScroll.create