Skip to content

Commit 3129dda

Browse files
authored
Add auto scroll (#4790)
* add auto_scroll * add auto_scroll * add auto_scroll to global * use random id for maximum safety
1 parent f4165c9 commit 3129dda

File tree

6 files changed

+218
-0
lines changed

6 files changed

+218
-0
lines changed

reflex/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@
248248
"selected_files",
249249
"upload",
250250
],
251+
"components.core.auto_scroll": ["auto_scroll"],
251252
}
252253

253254
COMPONENTS_BASE_MAPPING: dict = {

reflex/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ from .components.component import Component as Component
3434
from .components.component import ComponentNamespace as ComponentNamespace
3535
from .components.component import NoSSRComponent as NoSSRComponent
3636
from .components.component import memo as memo
37+
from .components.core.auto_scroll import auto_scroll as auto_scroll
3738
from .components.core.banner import connection_banner as connection_banner
3839
from .components.core.banner import connection_modal as connection_modal
3940
from .components.core.breakpoints import breakpoints as breakpoints

reflex/components/core/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"get_upload_url",
4949
"selected_files",
5050
],
51+
"auto_scroll": ["auto_scroll"],
5152
}
5253

5354
__getattr__, __dir__, __all__ = lazy_loader.attach(

reflex/components/core/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# ------------------------------------------------------
55

66
from . import layout as layout
7+
from .auto_scroll import auto_scroll as auto_scroll
78
from .banner import ConnectionBanner as ConnectionBanner
89
from .banner import ConnectionModal as ConnectionModal
910
from .banner import ConnectionPulser as ConnectionPulser
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""A component that automatically scrolls to the bottom when new content is added."""
2+
3+
from __future__ import annotations
4+
5+
from reflex.components.el.elements.typography import Div
6+
from reflex.constants.compiler import MemoizationDisposition, MemoizationMode
7+
from reflex.utils.imports import ImportDict
8+
from reflex.vars.base import Var, get_unique_variable_name
9+
10+
11+
class AutoScroll(Div):
12+
"""A div that automatically scrolls to the bottom when new content is added."""
13+
14+
_memoization_mode = MemoizationMode(disposition=MemoizationDisposition.ALWAYS)
15+
16+
@classmethod
17+
def create(cls, *children, **props):
18+
"""Create an AutoScroll component.
19+
20+
Args:
21+
*children: The children of the component.
22+
**props: The props of the component.
23+
24+
Returns:
25+
An AutoScroll component.
26+
"""
27+
props.setdefault("overflow", "auto")
28+
props.setdefault("id", get_unique_variable_name())
29+
return super().create(*children, **props)
30+
31+
def add_imports(self) -> ImportDict | list[ImportDict]:
32+
"""Add imports required for the component.
33+
34+
Returns:
35+
The imports required for the component.
36+
"""
37+
return {"react": ["useEffect", "useRef"]}
38+
39+
def add_hooks(self) -> list[str | Var]:
40+
"""Add hooks required for the component.
41+
42+
Returns:
43+
The hooks required for the component.
44+
"""
45+
ref_name = self.get_ref()
46+
return [
47+
"const containerRef = useRef(null);",
48+
"const wasNearBottom = useRef(false);",
49+
"const hadScrollbar = useRef(false);",
50+
f"""
51+
const checkIfNearBottom = () => {{
52+
if (!{ref_name}.current) return;
53+
54+
const container = {ref_name}.current;
55+
const nearBottomThreshold = 50; // pixels from bottom to trigger auto-scroll
56+
57+
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
58+
59+
wasNearBottom.current = distanceFromBottom <= nearBottomThreshold;
60+
61+
// Track if container had a scrollbar
62+
hadScrollbar.current = container.scrollHeight > container.clientHeight;
63+
}};
64+
""",
65+
f"""
66+
const scrollToBottomIfNeeded = () => {{
67+
if (!{ref_name}.current) return;
68+
69+
const container = {ref_name}.current;
70+
const hasScrollbarNow = container.scrollHeight > container.clientHeight;
71+
72+
// Scroll if:
73+
// 1. User was near bottom, OR
74+
// 2. Container didn't have scrollbar before but does now
75+
if (wasNearBottom.current || (!hadScrollbar.current && hasScrollbarNow)) {{
76+
container.scrollTop = container.scrollHeight;
77+
}}
78+
79+
// Update scrollbar state for next check
80+
hadScrollbar.current = hasScrollbarNow;
81+
}};
82+
""",
83+
f"""
84+
useEffect(() => {{
85+
const container = {ref_name}.current;
86+
if (!container) return;
87+
88+
// Create ResizeObserver to detect height changes
89+
const resizeObserver = new ResizeObserver(() => {{
90+
scrollToBottomIfNeeded();
91+
}});
92+
93+
// Track scroll position before height changes
94+
container.addEventListener('scroll', checkIfNearBottom);
95+
96+
// Initial check
97+
checkIfNearBottom();
98+
99+
// Observe container for size changes
100+
resizeObserver.observe(container);
101+
102+
return () => {{
103+
container.removeEventListener('scroll', checkIfNearBottom);
104+
resizeObserver.disconnect();
105+
}};
106+
}});
107+
""",
108+
]
109+
110+
111+
auto_scroll = AutoScroll.create
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""Stub file for reflex/components/core/auto_scroll.py"""
2+
3+
# ------------------- DO NOT EDIT ----------------------
4+
# This file was generated by `reflex/utils/pyi_generator.py`!
5+
# ------------------------------------------------------
6+
from typing import Any, Dict, Optional, Union, overload
7+
8+
from reflex.components.el.elements.typography import Div
9+
from reflex.event import EventType
10+
from reflex.style import Style
11+
from reflex.utils.imports import ImportDict
12+
from reflex.vars.base import Var
13+
14+
class AutoScroll(Div):
15+
@overload
16+
@classmethod
17+
def create( # type: ignore
18+
cls,
19+
*children,
20+
access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
21+
auto_capitalize: Optional[
22+
Union[Var[Union[bool, int, str]], bool, int, str]
23+
] = None,
24+
content_editable: Optional[
25+
Union[Var[Union[bool, int, str]], bool, int, str]
26+
] = None,
27+
context_menu: Optional[
28+
Union[Var[Union[bool, int, str]], bool, int, str]
29+
] = None,
30+
dir: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
31+
draggable: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
32+
enter_key_hint: Optional[
33+
Union[Var[Union[bool, int, str]], bool, int, str]
34+
] = None,
35+
hidden: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
36+
input_mode: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
37+
item_prop: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
38+
lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
39+
role: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
40+
slot: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
41+
spell_check: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
42+
tab_index: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
43+
title: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
44+
style: Optional[Style] = None,
45+
key: Optional[Any] = None,
46+
id: Optional[Any] = None,
47+
class_name: Optional[Any] = None,
48+
autofocus: Optional[bool] = None,
49+
custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None,
50+
on_blur: Optional[EventType[()]] = None,
51+
on_click: Optional[EventType[()]] = None,
52+
on_context_menu: Optional[EventType[()]] = None,
53+
on_double_click: Optional[EventType[()]] = None,
54+
on_focus: Optional[EventType[()]] = None,
55+
on_mount: Optional[EventType[()]] = None,
56+
on_mouse_down: Optional[EventType[()]] = None,
57+
on_mouse_enter: Optional[EventType[()]] = None,
58+
on_mouse_leave: Optional[EventType[()]] = None,
59+
on_mouse_move: Optional[EventType[()]] = None,
60+
on_mouse_out: Optional[EventType[()]] = None,
61+
on_mouse_over: Optional[EventType[()]] = None,
62+
on_mouse_up: Optional[EventType[()]] = None,
63+
on_scroll: Optional[EventType[()]] = None,
64+
on_unmount: Optional[EventType[()]] = None,
65+
**props,
66+
) -> "AutoScroll":
67+
"""Create an AutoScroll component.
68+
69+
Args:
70+
*children: The children of the component.
71+
access_key: Provides a hint for generating a keyboard shortcut for the current element.
72+
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
73+
content_editable: Indicates whether the element's content is editable.
74+
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
75+
dir: Defines the text direction. Allowed values are ltr (Left-To-Right) or rtl (Right-To-Left)
76+
draggable: Defines whether the element can be dragged.
77+
enter_key_hint: Hints what media types the media element is able to play.
78+
hidden: Defines whether the element is hidden.
79+
input_mode: Defines the type of the element.
80+
item_prop: Defines the name of the element for metadata purposes.
81+
lang: Defines the language used in the element.
82+
role: Defines the role of the element.
83+
slot: Assigns a slot in a shadow DOM shadow tree to an element.
84+
spell_check: Defines whether the element may be checked for spelling errors.
85+
tab_index: Defines the position of the current element in the tabbing order.
86+
title: Defines a tooltip for the element.
87+
style: The style of the component.
88+
key: A unique key for the component.
89+
id: The id for the component.
90+
class_name: The class name for the component.
91+
autofocus: Whether the component should take the focus once the page is loaded
92+
custom_attrs: custom attribute
93+
**props: The props of the component.
94+
95+
Returns:
96+
An AutoScroll component.
97+
"""
98+
...
99+
100+
def add_imports(self) -> ImportDict | list[ImportDict]: ...
101+
def add_hooks(self) -> list[str | Var]: ...
102+
103+
auto_scroll = AutoScroll.create

0 commit comments

Comments
 (0)