Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add auto scroll #4790

Merged
merged 4 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions reflex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@
"selected_files",
"upload",
],
"components.core.auto_scroll": ["auto_scroll"],
}

COMPONENTS_BASE_MAPPING: dict = {
Expand Down
1 change: 1 addition & 0 deletions reflex/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ from .components.component import Component as Component
from .components.component import ComponentNamespace as ComponentNamespace
from .components.component import NoSSRComponent as NoSSRComponent
from .components.component import memo as memo
from .components.core.auto_scroll import auto_scroll as auto_scroll
from .components.core.banner import connection_banner as connection_banner
from .components.core.banner import connection_modal as connection_modal
from .components.core.breakpoints import breakpoints as breakpoints
Expand Down
1 change: 1 addition & 0 deletions reflex/components/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"get_upload_url",
"selected_files",
],
"auto_scroll": ["auto_scroll"],
}

__getattr__, __dir__, __all__ = lazy_loader.attach(
Expand Down
1 change: 1 addition & 0 deletions reflex/components/core/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# ------------------------------------------------------

from . import layout as layout
from .auto_scroll import auto_scroll as auto_scroll
from .banner import ConnectionBanner as ConnectionBanner
from .banner import ConnectionModal as ConnectionModal
from .banner import ConnectionPulser as ConnectionPulser
Expand Down
110 changes: 110 additions & 0 deletions reflex/components/core/auto_scroll.py
Original file line number Diff line number Diff line change
@@ -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
103 changes: 103 additions & 0 deletions reflex/components/core/auto_scroll.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Stub file for reflex/components/core/auto_scroll.py"""

# ------------------- DO NOT EDIT ----------------------
# This file was generated by `reflex/utils/pyi_generator.py`!
# ------------------------------------------------------
from typing import Any, Dict, Optional, Union, overload

from reflex.components.el.elements.typography import Div
from reflex.event import EventType
from reflex.style import Style
from reflex.utils.imports import ImportDict
from reflex.vars.base import Var

class AutoScroll(Div):
@overload
@classmethod
def create( # type: ignore
cls,
*children,
access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
auto_capitalize: Optional[
Union[Var[Union[bool, int, str]], bool, int, str]
] = None,
content_editable: Optional[
Union[Var[Union[bool, int, str]], bool, int, str]
] = None,
context_menu: Optional[
Union[Var[Union[bool, int, str]], bool, int, str]
] = None,
dir: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
draggable: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
enter_key_hint: Optional[
Union[Var[Union[bool, int, str]], bool, int, str]
] = None,
hidden: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
input_mode: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
item_prop: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
role: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
slot: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
spell_check: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
tab_index: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
title: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
style: Optional[Style] = None,
key: Optional[Any] = None,
id: Optional[Any] = None,
class_name: Optional[Any] = None,
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None,
on_blur: Optional[EventType[()]] = None,
on_click: Optional[EventType[()]] = None,
on_context_menu: Optional[EventType[()]] = None,
on_double_click: Optional[EventType[()]] = None,
on_focus: Optional[EventType[()]] = None,
on_mount: Optional[EventType[()]] = None,
on_mouse_down: Optional[EventType[()]] = None,
on_mouse_enter: Optional[EventType[()]] = None,
on_mouse_leave: Optional[EventType[()]] = None,
on_mouse_move: Optional[EventType[()]] = None,
on_mouse_out: Optional[EventType[()]] = None,
on_mouse_over: Optional[EventType[()]] = None,
on_mouse_up: Optional[EventType[()]] = None,
on_scroll: Optional[EventType[()]] = None,
on_unmount: Optional[EventType[()]] = None,
**props,
) -> "AutoScroll":
"""Create an AutoScroll component.

Args:
*children: The children of the component.
access_key: Provides a hint for generating a keyboard shortcut for the current element.
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
content_editable: Indicates whether the element's content is editable.
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
dir: Defines the text direction. Allowed values are ltr (Left-To-Right) or rtl (Right-To-Left)
draggable: Defines whether the element can be dragged.
enter_key_hint: Hints what media types the media element is able to play.
hidden: Defines whether the element is hidden.
input_mode: Defines the type of the element.
item_prop: Defines the name of the element for metadata purposes.
lang: Defines the language used in the element.
role: Defines the role of the element.
slot: Assigns a slot in a shadow DOM shadow tree to an element.
spell_check: Defines whether the element may be checked for spelling errors.
tab_index: Defines the position of the current element in the tabbing order.
title: Defines a tooltip for the element.
style: The style of the component.
key: A unique key for the component.
id: The id for the component.
class_name: The class name for the component.
autofocus: Whether the component should take the focus once the page is loaded
custom_attrs: custom attribute
**props: The props of the component.

Returns:
An AutoScroll component.
"""
...

def add_imports(self) -> ImportDict | list[ImportDict]: ...
def add_hooks(self) -> list[str | Var]: ...

auto_scroll = AutoScroll.create