123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- """Sonner toast component."""
- from __future__ import annotations
- from typing import Any, ClassVar, Literal, Optional, Union
- from reflex.base import Base
- from reflex.components.component import Component, ComponentNamespace
- from reflex.components.lucide.icon import Icon
- from reflex.components.props import NoExtrasAllowedProps, PropsBase
- from reflex.event import EventSpec, run_script
- from reflex.style import Style, resolved_color_mode
- from reflex.utils import format
- from reflex.utils.imports import ImportVar
- from reflex.utils.serializers import serializer
- from reflex.vars import VarData
- from reflex.vars.base import LiteralVar, Var
- from reflex.vars.function import FunctionVar
- from reflex.vars.object import ObjectVar
- LiteralPosition = Literal[
- "top-left",
- "top-center",
- "top-right",
- "bottom-left",
- "bottom-center",
- "bottom-right",
- ]
- toast_ref = Var(_js_expr="refs['__toast']")
- class ToastAction(Base):
- """A toast action that render a button in the toast."""
- label: str
- on_click: Any
- @serializer
- def serialize_action(action: ToastAction) -> dict:
- """Serialize a toast action.
- Args:
- action: The toast action to serialize.
- Returns:
- The serialized toast action with on_click formatted to queue the given event.
- """
- return {
- "label": action.label,
- "onClick": format.format_queue_events(action.on_click),
- }
- def _toast_callback_signature(toast: Var) -> list[Var]:
- """The signature for the toast callback, stripping out unserializable keys.
- Args:
- toast: The toast variable.
- Returns:
- A function call stripping non-serializable members of the toast object.
- """
- return [
- Var(
- _js_expr=f"(() => {{let {{action, cancel, onDismiss, onAutoClose, ...rest}} = {toast!s}; return rest}})()"
- )
- ]
- class ToastProps(PropsBase, NoExtrasAllowedProps):
- """Props for the toast component."""
- # Toast's title, renders above the description.
- title: Optional[Union[str, Var]]
- # Toast's description, renders underneath the title.
- description: Optional[Union[str, Var]]
- # Whether to show the close button.
- close_button: Optional[bool]
- # Dark toast in light mode and vice versa.
- invert: Optional[bool]
- # Control the sensitivity of the toast for screen readers
- important: Optional[bool]
- # Time in milliseconds that should elapse before automatically closing the toast.
- duration: Optional[int]
- # Position of the toast.
- position: Optional[LiteralPosition]
- # If false, it'll prevent the user from dismissing the toast.
- dismissible: Optional[bool]
- # TODO: fix serialization of icons for toast? (might not be possible yet)
- # Icon displayed in front of toast's text, aligned vertically.
- # icon: Optional[Icon] = None # noqa: ERA001
- # TODO: fix implementation for action / cancel buttons
- # Renders a primary button, clicking it will close the toast.
- action: Optional[ToastAction]
- # Renders a secondary button, clicking it will close the toast.
- cancel: Optional[ToastAction]
- # Custom id for the toast.
- id: Optional[Union[str, Var]]
- # Removes the default styling, which allows for easier customization.
- unstyled: Optional[bool]
- # Custom style for the toast.
- style: Optional[Style]
- # Class name for the toast.
- class_name: Optional[str]
- # XXX: These still do not seem to work
- # Custom style for the toast primary button.
- action_button_styles: Optional[Style]
- # Custom style for the toast secondary button.
- cancel_button_styles: Optional[Style]
- # The function gets called when either the close button is clicked, or the toast is swiped.
- on_dismiss: Optional[Any]
- # Function that gets called when the toast disappears automatically after it's timeout (duration` prop).
- on_auto_close: Optional[Any]
- def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
- """Convert the object to a dictionary.
- Args:
- *args: The arguments to pass to the base class.
- **kwargs: The keyword arguments to pass to the base
- Returns:
- The object as a dictionary with ToastAction fields intact.
- """
- kwargs.setdefault("exclude_none", True)
- d = super().dict(*args, **kwargs)
- # Keep these fields as ToastAction so they can be serialized specially
- if "action" in d:
- d["action"] = self.action
- if isinstance(self.action, dict):
- d["action"] = ToastAction(**self.action)
- if "cancel" in d:
- d["cancel"] = self.cancel
- if isinstance(self.cancel, dict):
- d["cancel"] = ToastAction(**self.cancel)
- if "onDismiss" in d:
- d["onDismiss"] = format.format_queue_events(
- self.on_dismiss, _toast_callback_signature
- )
- if "onAutoClose" in d:
- d["onAutoClose"] = format.format_queue_events(
- self.on_auto_close, _toast_callback_signature
- )
- return d
- class Toaster(Component):
- """A Toaster Component for displaying toast notifications."""
- library: str | None = "sonner@1.7.2"
- tag = "Toaster"
- # the theme of the toast
- theme: Var[str] = resolved_color_mode
- # whether to show rich colors
- rich_colors: Var[bool] = LiteralVar.create(True)
- # whether to expand the toast
- expand: Var[bool] = LiteralVar.create(True)
- # the number of toasts that are currently visible
- visible_toasts: Var[int]
- # the position of the toast
- position: Var[LiteralPosition] = LiteralVar.create("bottom-right")
- # whether to show the close button
- close_button: Var[bool] = LiteralVar.create(False)
- # offset of the toast
- offset: Var[str]
- # directionality of the toast (default: ltr)
- dir: Var[str]
- # Keyboard shortcut that will move focus to the toaster area.
- hotkey: Var[str]
- # Dark toasts in light mode and vice versa.
- invert: Var[bool]
- # These will act as default options for all toasts. See toast() for all available options.
- toast_options: Var[ToastProps]
- # Gap between toasts when expanded
- gap: Var[int]
- # Changes the default loading icon
- loading_icon: Var[Icon]
- # Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked.
- pause_when_page_is_hidden: Var[bool]
- # Marked True when any Toast component is created.
- is_used: ClassVar[bool] = False
- def add_hooks(self) -> list[Var | str]:
- """Add hooks for the toaster component.
- Returns:
- The hooks for the toaster component.
- """
- if self.library is None:
- return []
- hook = Var(
- _js_expr=f"{toast_ref} = toast",
- _var_data=VarData(
- imports={
- "$/utils/state": [ImportVar(tag="refs")],
- self.library: [ImportVar(tag="toast", install=False)],
- }
- ),
- )
- return [hook]
- @staticmethod
- def send_toast(
- message: str | Var = "", level: str | None = None, **props
- ) -> EventSpec:
- """Send a toast message.
- Args:
- message: The message to display.
- level: The level of the toast.
- **props: The options for the toast.
- Raises:
- ValueError: If the Toaster component is not created.
- Returns:
- The toast event.
- """
- if not Toaster.is_used:
- raise ValueError(
- "Toaster component must be created before sending a toast. (use `rx.toast.provider()`)"
- )
- toast_command = (
- ObjectVar.__getattr__(toast_ref.to(dict), level) if level else toast_ref
- ).to(FunctionVar)
- if isinstance(message, Var):
- props.setdefault("title", message)
- message = ""
- elif message == "" and "title" not in props and "description" not in props:
- raise ValueError("Toast message or title or description must be provided.")
- if props:
- args = LiteralVar.create(ToastProps(component_name="rx.toast", **props)) # pyright: ignore [reportCallIssue]
- toast = toast_command.call(message, args)
- else:
- toast = toast_command.call(message)
- return run_script(toast)
- @staticmethod
- def toast_info(message: str | Var = "", **kwargs: Any):
- """Display an info toast message.
- Args:
- message: The message to display.
- **kwargs: Additional toast props.
- Returns:
- The toast event.
- """
- return Toaster.send_toast(message, level="info", **kwargs)
- @staticmethod
- def toast_warning(message: str | Var = "", **kwargs: Any):
- """Display a warning toast message.
- Args:
- message: The message to display.
- **kwargs: Additional toast props.
- Returns:
- The toast event.
- """
- return Toaster.send_toast(message, level="warning", **kwargs)
- @staticmethod
- def toast_error(message: str | Var = "", **kwargs: Any):
- """Display an error toast message.
- Args:
- message: The message to display.
- **kwargs: Additional toast props.
- Returns:
- The toast event.
- """
- return Toaster.send_toast(message, level="error", **kwargs)
- @staticmethod
- def toast_success(message: str | Var = "", **kwargs: Any):
- """Display a success toast message.
- Args:
- message: The message to display.
- **kwargs: Additional toast props.
- Returns:
- The toast event.
- """
- return Toaster.send_toast(message, level="success", **kwargs)
- @staticmethod
- def toast_dismiss(id: Var | str | None = None):
- """Dismiss a toast.
- Args:
- id: The id of the toast to dismiss.
- Returns:
- The toast dismiss event.
- """
- dismiss_var_data = None
- if isinstance(id, Var):
- dismiss = f"{toast_ref}.dismiss({id!s})"
- dismiss_var_data = id._get_all_var_data()
- elif isinstance(id, str):
- dismiss = f"{toast_ref}.dismiss('{id}')"
- else:
- dismiss = f"{toast_ref}.dismiss()"
- dismiss_action = Var(
- _js_expr=dismiss, _var_data=VarData.merge(dismiss_var_data)
- )
- return run_script(dismiss_action)
- @classmethod
- def create(cls, *children: Any, **props: Any) -> Component:
- """Create a toaster component.
- Args:
- *children: The children of the toaster.
- **props: The properties of the toaster.
- Returns:
- The toaster component.
- """
- cls.is_used = True
- return super().create(*children, **props)
- # TODO: figure out why loading toast stay open forever when using level="loading" in toast()
- class ToastNamespace(ComponentNamespace):
- """Namespace for toast components."""
- provider = staticmethod(Toaster.create)
- options = staticmethod(ToastProps)
- info = staticmethod(Toaster.toast_info)
- warning = staticmethod(Toaster.toast_warning)
- error = staticmethod(Toaster.toast_error)
- success = staticmethod(Toaster.toast_success)
- dismiss = staticmethod(Toaster.toast_dismiss)
- __call__ = staticmethod(Toaster.send_toast)
- toast = ToastNamespace()
|