toast.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. """Sonner toast component."""
  2. from __future__ import annotations
  3. from typing import Any, Literal, Optional
  4. from reflex.base import Base
  5. from reflex.components.component import Component, ComponentNamespace
  6. from reflex.components.lucide.icon import Icon
  7. from reflex.components.props import PropsBase
  8. from reflex.event import (
  9. EventSpec,
  10. call_script,
  11. )
  12. from reflex.style import Style, color_mode
  13. from reflex.utils import format
  14. from reflex.utils.imports import ImportVar
  15. from reflex.utils.serializers import serialize, serializer
  16. from reflex.vars import Var, VarData
  17. LiteralPosition = Literal[
  18. "top-left",
  19. "top-center",
  20. "top-right",
  21. "bottom-left",
  22. "bottom-center",
  23. "bottom-right",
  24. ]
  25. toast_ref = Var.create_safe("refs['__toast']")
  26. class ToastAction(Base):
  27. """A toast action that render a button in the toast."""
  28. label: str
  29. on_click: Any
  30. @serializer
  31. def serialize_action(action: ToastAction) -> dict:
  32. """Serialize a toast action.
  33. Args:
  34. action: The toast action to serialize.
  35. Returns:
  36. The serialized toast action with on_click formatted to queue the given event.
  37. """
  38. return {
  39. "label": action.label,
  40. "onClick": format.format_queue_events(action.on_click),
  41. }
  42. def _toast_callback_signature(toast: Var) -> list[Var]:
  43. """The signature for the toast callback, stripping out unserializable keys.
  44. Args:
  45. toast: The toast variable.
  46. Returns:
  47. A function call stripping non-serializable members of the toast object.
  48. """
  49. return [
  50. Var.create_safe(
  51. f"(() => {{let {{action, cancel, onDismiss, onAutoClose, ...rest}} = {toast}; return rest}})()"
  52. )
  53. ]
  54. class ToastProps(PropsBase):
  55. """Props for the toast component."""
  56. # Toast's description, renders underneath the title.
  57. description: Optional[str]
  58. # Whether to show the close button.
  59. close_button: Optional[bool]
  60. # Dark toast in light mode and vice versa.
  61. invert: Optional[bool]
  62. # Control the sensitivity of the toast for screen readers
  63. important: Optional[bool]
  64. # Time in milliseconds that should elapse before automatically closing the toast.
  65. duration: Optional[int]
  66. # Position of the toast.
  67. position: Optional[LiteralPosition]
  68. # If false, it'll prevent the user from dismissing the toast.
  69. dismissible: Optional[bool]
  70. # TODO: fix serialization of icons for toast? (might not be possible yet)
  71. # Icon displayed in front of toast's text, aligned vertically.
  72. # icon: Optional[Icon] = None
  73. # TODO: fix implementation for action / cancel buttons
  74. # Renders a primary button, clicking it will close the toast.
  75. action: Optional[ToastAction]
  76. # Renders a secondary button, clicking it will close the toast.
  77. cancel: Optional[ToastAction]
  78. # Custom id for the toast.
  79. id: Optional[str]
  80. # Removes the default styling, which allows for easier customization.
  81. unstyled: Optional[bool]
  82. # Custom style for the toast.
  83. style: Optional[Style]
  84. # XXX: These still do not seem to work
  85. # Custom style for the toast primary button.
  86. action_button_styles: Optional[Style]
  87. # Custom style for the toast secondary button.
  88. cancel_button_styles: Optional[Style]
  89. # The function gets called when either the close button is clicked, or the toast is swiped.
  90. on_dismiss: Optional[Any]
  91. # Function that gets called when the toast disappears automatically after it's timeout (duration` prop).
  92. on_auto_close: Optional[Any]
  93. def dict(self, *args, **kwargs) -> dict:
  94. """Convert the object to a dictionary.
  95. Args:
  96. *args: The arguments to pass to the base class.
  97. **kwargs: The keyword arguments to pass to the base
  98. Returns:
  99. The object as a dictionary with ToastAction fields intact.
  100. """
  101. kwargs.setdefault("exclude_none", True)
  102. d = super().dict(*args, **kwargs)
  103. # Keep these fields as ToastAction so they can be serialized specially
  104. if "action" in d:
  105. d["action"] = self.action
  106. if isinstance(self.action, dict):
  107. d["action"] = ToastAction(**self.action)
  108. if "cancel" in d:
  109. d["cancel"] = self.cancel
  110. if isinstance(self.cancel, dict):
  111. d["cancel"] = ToastAction(**self.cancel)
  112. if "on_dismiss" in d:
  113. d["on_dismiss"] = format.format_queue_events(
  114. self.on_dismiss, _toast_callback_signature
  115. )
  116. if "on_auto_close" in d:
  117. d["on_auto_close"] = format.format_queue_events(
  118. self.on_auto_close, _toast_callback_signature
  119. )
  120. return d
  121. class Toaster(Component):
  122. """A Toaster Component for displaying toast notifications."""
  123. library = "sonner@1.4.41"
  124. tag = "Toaster"
  125. # the theme of the toast
  126. theme: Var[str] = color_mode
  127. # whether to show rich colors
  128. rich_colors: Var[bool] = Var.create_safe(True)
  129. # whether to expand the toast
  130. expand: Var[bool] = Var.create_safe(True)
  131. # the number of toasts that are currently visible
  132. visible_toasts: Var[int]
  133. # the position of the toast
  134. position: Var[LiteralPosition] = Var.create_safe("bottom-right")
  135. # whether to show the close button
  136. close_button: Var[bool] = Var.create_safe(False)
  137. # offset of the toast
  138. offset: Var[str]
  139. # directionality of the toast (default: ltr)
  140. dir: Var[str]
  141. # Keyboard shortcut that will move focus to the toaster area.
  142. hotkey: Var[str]
  143. # Dark toasts in light mode and vice versa.
  144. invert: Var[bool]
  145. # These will act as default options for all toasts. See toast() for all available options.
  146. toast_options: Var[ToastProps]
  147. # Gap between toasts when expanded
  148. gap: Var[int]
  149. # Changes the default loading icon
  150. loading_icon: Var[Icon]
  151. # Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked.
  152. pause_when_page_is_hidden: Var[bool]
  153. def _get_hooks(self) -> Var[str]:
  154. hook = Var.create_safe(f"{toast_ref} = toast", _var_is_local=True)
  155. hook._var_data = VarData( # type: ignore
  156. imports={
  157. "/utils/state": [ImportVar(tag="refs")],
  158. self.library: [ImportVar(tag="toast", install=False)],
  159. }
  160. )
  161. return hook
  162. @staticmethod
  163. def send_toast(message: str, level: str | None = None, **props) -> EventSpec:
  164. """Send a toast message.
  165. Args:
  166. message: The message to display.
  167. level: The level of the toast.
  168. **props: The options for the toast.
  169. Returns:
  170. The toast event.
  171. """
  172. toast_command = f"{toast_ref}.{level}" if level is not None else toast_ref
  173. if props:
  174. args = serialize(ToastProps(**props))
  175. toast = f"{toast_command}(`{message}`, {args})"
  176. else:
  177. toast = f"{toast_command}(`{message}`)"
  178. toast_action = Var.create(toast, _var_is_string=False, _var_is_local=True)
  179. return call_script(toast_action) # type: ignore
  180. @staticmethod
  181. def toast_info(message: str, **kwargs):
  182. """Display an info toast message.
  183. Args:
  184. message: The message to display.
  185. kwargs: Additional toast props.
  186. Returns:
  187. The toast event.
  188. """
  189. return Toaster.send_toast(message, level="info", **kwargs)
  190. @staticmethod
  191. def toast_warning(message: str, **kwargs):
  192. """Display a warning toast message.
  193. Args:
  194. message: The message to display.
  195. kwargs: Additional toast props.
  196. Returns:
  197. The toast event.
  198. """
  199. return Toaster.send_toast(message, level="warning", **kwargs)
  200. @staticmethod
  201. def toast_error(message: str, **kwargs):
  202. """Display an error toast message.
  203. Args:
  204. message: The message to display.
  205. kwargs: Additional toast props.
  206. Returns:
  207. The toast event.
  208. """
  209. return Toaster.send_toast(message, level="error", **kwargs)
  210. @staticmethod
  211. def toast_success(message: str, **kwargs):
  212. """Display a success toast message.
  213. Args:
  214. message: The message to display.
  215. kwargs: Additional toast props.
  216. Returns:
  217. The toast event.
  218. """
  219. return Toaster.send_toast(message, level="success", **kwargs)
  220. def toast_dismiss(self, id: str | None):
  221. """Dismiss a toast.
  222. Args:
  223. id: The id of the toast to dismiss.
  224. Returns:
  225. The toast dismiss event.
  226. """
  227. if id is None:
  228. dismiss = f"{toast_ref}.dismiss()"
  229. else:
  230. dismiss = f"{toast_ref}.dismiss({id})"
  231. dismiss_action = Var.create(dismiss, _var_is_string=False, _var_is_local=True)
  232. return call_script(dismiss_action) # type: ignore
  233. # TODO: figure out why loading toast stay open forever
  234. # def toast_loading(message: str, **kwargs):
  235. # return _toast(message, level="loading", **kwargs)
  236. class ToastNamespace(ComponentNamespace):
  237. """Namespace for toast components."""
  238. provider = staticmethod(Toaster.create)
  239. options = staticmethod(ToastProps)
  240. info = staticmethod(Toaster.toast_info)
  241. warning = staticmethod(Toaster.toast_warning)
  242. error = staticmethod(Toaster.toast_error)
  243. success = staticmethod(Toaster.toast_success)
  244. dismiss = staticmethod(Toaster.toast_dismiss)
  245. # loading = staticmethod(toast_loading)
  246. __call__ = staticmethod(Toaster.send_toast)
  247. toast = ToastNamespace()