auto_scroll.py 3.3 KB

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