auto_scroll.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. """A component that automatically scrolls to the bottom when new content is added."""
  2. from __future__ import annotations
  3. from reflex.components.el.elements.typography import Div
  4. from reflex.constants.compiler import MemoizationDisposition, MemoizationMode
  5. from reflex.utils.imports import ImportDict
  6. from reflex.vars.base import Var, get_unique_variable_name
  7. class AutoScroll(Div):
  8. """A div that automatically scrolls to the bottom when new content is added."""
  9. _memoization_mode = MemoizationMode(
  10. disposition=MemoizationDisposition.ALWAYS, recursive=False
  11. )
  12. @classmethod
  13. def create(cls, *children, **props):
  14. """Create an AutoScroll component.
  15. Args:
  16. *children: The children of the component.
  17. **props: The props of the component.
  18. Returns:
  19. An AutoScroll component.
  20. """
  21. props.setdefault("overflow", "auto")
  22. props.setdefault("id", get_unique_variable_name())
  23. return super().create(*children, **props)
  24. def add_imports(self) -> ImportDict | list[ImportDict]:
  25. """Add imports required for the component.
  26. Returns:
  27. The imports required for the component.
  28. """
  29. return {"react": ["useEffect", "useRef"]}
  30. def add_hooks(self) -> list[str | Var]:
  31. """Add hooks required for the component.
  32. Returns:
  33. The hooks required for the component.
  34. """
  35. ref_name = self.get_ref()
  36. return [
  37. "const wasNearBottom = useRef(false);",
  38. "const hadScrollbar = useRef(false);",
  39. f"""
  40. const checkIfNearBottom = () => {{
  41. if (!{ref_name}.current) return;
  42. const container = {ref_name}.current;
  43. const nearBottomThreshold = 50; // pixels from bottom to trigger auto-scroll
  44. const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
  45. wasNearBottom.current = distanceFromBottom <= nearBottomThreshold;
  46. // Track if container had a scrollbar
  47. hadScrollbar.current = container.scrollHeight > container.clientHeight;
  48. }};
  49. """,
  50. f"""
  51. const scrollToBottomIfNeeded = () => {{
  52. if (!{ref_name}.current) return;
  53. const container = {ref_name}.current;
  54. const hasScrollbarNow = container.scrollHeight > container.clientHeight;
  55. // Scroll if:
  56. // 1. User was near bottom, OR
  57. // 2. Container didn't have scrollbar before but does now
  58. if (wasNearBottom.current || (!hadScrollbar.current && hasScrollbarNow)) {{
  59. container.scrollTop = container.scrollHeight;
  60. }}
  61. // Update scrollbar state for next check
  62. hadScrollbar.current = hasScrollbarNow;
  63. }};
  64. """,
  65. f"""
  66. useEffect(() => {{
  67. const container = {ref_name}.current;
  68. if (!container) return;
  69. scrollToBottomIfNeeded();
  70. // Create ResizeObserver to detect height changes
  71. const resizeObserver = new ResizeObserver(() => {{
  72. scrollToBottomIfNeeded();
  73. }});
  74. // Track scroll position before height changes
  75. container.addEventListener('scroll', checkIfNearBottom);
  76. // Initial check
  77. checkIfNearBottom();
  78. // Observe container for size changes
  79. resizeObserver.observe(container);
  80. return () => {{
  81. container.removeEventListener('scroll', checkIfNearBottom);
  82. resizeObserver.disconnect();
  83. }};
  84. }});
  85. """,
  86. ]
  87. auto_scroll = AutoScroll.create