diff --git a/pyi_hashes.json b/pyi_hashes.json index fc25c93b890..6c42a4e1887 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -1,5 +1,5 @@ { - "reflex/__init__.pyi": "c69e4120c505941af8087b9c18df6121", + "reflex/__init__.pyi": "8f9482b205f5a33a59f748006ded637b", "reflex/components/__init__.pyi": "ac05995852baa81062ba3d18fbc489fb", "reflex/components/base/__init__.pyi": "16e47bf19e0d62835a605baa3d039c5a", "reflex/components/base/app_wrap.pyi": "ae600e2cc9d70f2ce613bdd6c1da3b16", @@ -11,7 +11,7 @@ "reflex/components/base/meta.pyi": "0445c66fbc32671c795640ef1a4827e9", "reflex/components/base/script.pyi": "43a0e21f257b10d2c76ed359284a9d80", "reflex/components/base/strict_mode.pyi": "d169c575d676c73edc6a3f593badfd1f", - "reflex/components/core/__init__.pyi": "6419485660830fe0af9c9c5715a4ed03", + "reflex/components/core/__init__.pyi": "007170b97e58bdf28b2aee381d91c0c7", "reflex/components/core/auto_scroll.pyi": "c628ed503c7bfcee0dd05cf48d5b763d", "reflex/components/core/banner.pyi": "407352aa1833b80b21d30647ec7717d8", "reflex/components/core/client_side_routing.pyi": "c3d38a1de89cfcd76735a1559e99ed05", @@ -21,6 +21,7 @@ "reflex/components/core/html.pyi": "faf9bb353ef4784e7f17ac97c93ef697", "reflex/components/core/sticky.pyi": "cdf17e6cd287e7300acd25669701d117", "reflex/components/core/upload.pyi": "f9be9b74d97d841b53b963d8704d5809", + "reflex/components/core/window_events.pyi": "04be3d73886d12774498004b712c136e", "reflex/components/datadisplay/__init__.pyi": "52755871369acbfd3a96b46b9a11d32e", "reflex/components/datadisplay/code.pyi": "3787ca724cae7b29d57ea03f981b8c22", "reflex/components/datadisplay/dataeditor.pyi": "23f777b8a46eff2afd95035dd5fc51a7", diff --git a/reflex/__init__.py b/reflex/__init__.py index 58dabbd02f0..5d3daff80de 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -249,6 +249,7 @@ "upload", ], "components.core.auto_scroll": ["auto_scroll"], + "components.core.window_events": ["window_event_listener"], } COMPONENTS_BASE_MAPPING: dict = { diff --git a/reflex/components/core/__init__.py b/reflex/components/core/__init__.py index e720e8667fd..c9babf294a1 100644 --- a/reflex/components/core/__init__.py +++ b/reflex/components/core/__init__.py @@ -50,6 +50,7 @@ "selected_files", ], "auto_scroll": ["auto_scroll"], + "window_events": ["WindowEventListener", "window_event_listener"], } __getattr__, __dir__, __all__ = lazy_loader.attach( diff --git a/reflex/components/core/window_events.py b/reflex/components/core/window_events.py new file mode 100644 index 00000000000..76fd1eac9bb --- /dev/null +++ b/reflex/components/core/window_events.py @@ -0,0 +1,104 @@ +"""Window event listener component for Reflex.""" + +import reflex as rx +from reflex.components.base.fragment import Fragment +from reflex.constants.compiler import Hooks +from reflex.event import key_event, no_args_event_spec +from reflex.vars.base import Var, VarData +from reflex.vars.object import ObjectVar + + +def _on_resize_spec() -> tuple[Var[int], Var[int]]: + """Args spec for the on_resize event trigger. + + Returns: + A tuple containing window width and height variables. + """ + return (Var("window.innerWidth"), Var("window.innerHeight")) + + +def _on_scroll_spec() -> tuple[Var[float], Var[float]]: + """Args spec for the on_scroll event trigger. + + Returns: + A tuple containing window scroll X and Y position variables. + """ + return (Var("window.scrollX"), Var("window.scrollY")) + + +def _on_visibility_change_spec() -> tuple[Var[bool]]: + """Args spec for the on_visibility_change event trigger. + + Returns: + A tuple containing the document hidden state variable. + """ + return (Var("document.hidden"),) + + +def _on_storage_spec(e: ObjectVar) -> tuple[Var[str], Var[str], Var[str], Var[str]]: + """Args spec for the on_storage event trigger. + + Args: + e: The storage event. + + Returns: + A tuple containing key, old value, new value, and URL variables. + """ + return (e.key.to(str), e.oldValue.to(str), e.newValue.to(str), e.url.to(str)) + + +class WindowEventListener(Fragment): + """A component that listens for window events.""" + + # Event handlers + on_resize: rx.EventHandler[_on_resize_spec] + on_scroll: rx.EventHandler[_on_scroll_spec] + on_focus: rx.EventHandler[no_args_event_spec] + on_blur: rx.EventHandler[no_args_event_spec] + on_visibility_change: rx.EventHandler[_on_visibility_change_spec] + on_before_unload: rx.EventHandler[no_args_event_spec] + on_key_down: rx.EventHandler[key_event] + on_popstate: rx.EventHandler[no_args_event_spec] + on_storage: rx.EventHandler[_on_storage_spec] + + def _exclude_props(self) -> list[str]: + """Exclude event handler props from being passed to Fragment. + + Returns: + List of prop names to exclude from the Fragment. + """ + return [*super()._exclude_props(), *self.event_triggers.keys()] + + def add_hooks(self) -> list[str | Var[str]]: + """Add hooks to register window event listeners. + + Returns: + The hooks to add to the component. + """ + hooks = [] + + for prop_name, event_trigger in self.event_triggers.items(): + # Get JS event name: remove on_ prefix and underscores + event_name = prop_name.removeprefix("on_").replace("_", "") + + hook_expr = f""" + useEffect(() => {{ + if (typeof window === 'undefined') return; + + window.addEventListener('{event_name}', {event_trigger}); + return () => window.removeEventListener('{event_name}', {event_trigger}); + }}, []); + """ + + hooks.append( + Var( + hook_expr, + _var_type="str", + _var_data=VarData(position=Hooks.HookPosition.POST_TRIGGER), + ) + ) + + return hooks + + +window_event_listener = WindowEventListener.create