auto_scroll.py 3.5 KB

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