add toast component (#3186)

This commit is contained in:
Thomas Brandého 2024-05-03 21:09:11 +02:00 committed by GitHub
parent 7903a1020d
commit 1817c30e22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 600 additions and 18 deletions

View File

@ -111,6 +111,7 @@ _ALL_COMPONENTS = [
"ordered_list",
"moment",
"logo",
"toast",
]
_MAPPING = {

View File

@ -98,6 +98,7 @@ from reflex.components import unordered_list as unordered_list
from reflex.components import ordered_list as ordered_list
from reflex.components import moment as moment
from reflex.components import logo as logo
from reflex.components import toast as toast
from reflex.components.component import Component as Component
from reflex.components.component import NoSSRComponent as NoSSRComponent
from reflex.components.component import memo as memo

View File

@ -15,6 +15,7 @@ from .next import NextLink, next_link
from .plotly import *
from .radix import *
from .react_player import *
from .sonner import *
from .suneditor import *
icon = lucide.icon

View File

@ -23,9 +23,7 @@ class List(ChakraComponent):
style_type: Var[str]
@classmethod
def create(
cls, *children, items: list | Var[list] | None = None, **props
) -> Component:
def create(cls, *children, items: Var[list] | None = None, **props) -> Component:
"""Create a list component.
Args:

View File

@ -18,7 +18,7 @@ class List(ChakraComponent):
def create( # type: ignore
cls,
*children,
items: Optional[list | Var[list] | None] = None,
items: Optional[Union[Var[list], list]] = None,
spacing: Optional[Union[Var[str], str]] = None,
style_position: Optional[Union[Var[str], str]] = None,
style_type: Optional[Union[Var[str], str]] = None,
@ -178,7 +178,7 @@ class OrderedList(List):
def create( # type: ignore
cls,
*children,
items: Optional[list | Var[list] | None] = None,
items: Optional[Union[Var[list], list]] = None,
spacing: Optional[Union[Var[str], str]] = None,
style_position: Optional[Union[Var[str], str]] = None,
style_type: Optional[Union[Var[str], str]] = None,
@ -262,7 +262,7 @@ class UnorderedList(List):
def create( # type: ignore
cls,
*children,
items: Optional[list | Var[list] | None] = None,
items: Optional[Union[Var[list], list]] = None,
spacing: Optional[Union[Var[str], str]] = None,
style_position: Optional[Union[Var[str], str]] = None,
style_type: Optional[Union[Var[str], str]] = None,

View File

@ -147,7 +147,7 @@ class PinInputField(ChakraComponent):
def create( # type: ignore
cls,
*children,
index: Optional[Var[int]] = None,
index: Optional[Union[Var[int], int]] = None,
name: Optional[Union[Var[str], str]] = None,
style: Optional[Style] = None,
key: Optional[Any] = None,

View File

@ -314,10 +314,10 @@ class AccordionRoot(AccordionComponent):
type: Var[LiteralAccordionType]
# The value of the item to expand.
value: Var[Optional[Union[str, List[str]]]]
value: Var[Union[str, List[str]]]
# The default value of the item to expand.
default_value: Var[Optional[Union[str, List[str]]]]
default_value: Var[Union[str, List[str]]]
# Whether or not the accordion is collapsible.
collapsible: Var[bool]

View File

@ -303,8 +303,8 @@ class AccordionItem(AccordionComponent):
def create( # type: ignore
cls,
*children,
header: Optional[Component | Var] = None,
content: Optional[Component | Var] = None,
header: Optional[Union[Component, Var]] = None,
content: Optional[Union[Component, Var]] = None,
value: Optional[Union[Var[str], str]] = None,
disabled: Optional[Union[Var[bool], bool]] = None,
as_child: Optional[Union[Var[bool], bool]] = None,

View File

@ -409,7 +409,7 @@ class Theme(RadixThemesComponent):
def create( # type: ignore
cls,
*children,
color_mode: Optional[LiteralAppearance | None] = None,
color_mode: Optional[Literal["inherit", "light", "dark"]] = None,
theme_panel: Optional[bool] = False,
has_background: Optional[Union[Var[bool], bool]] = None,
appearance: Optional[

View File

@ -109,7 +109,9 @@ class ColorModeIconButton(IconButton):
def create( # type: ignore
cls,
*children,
position: Optional[LiteralPosition | None] = None,
position: Optional[
Literal["top-left", "top-right", "bottom-left", "bottom-right"]
] = None,
as_child: Optional[Union[Var[bool], bool]] = None,
size: Optional[
Union[Var[Literal["1", "2", "3", "4"]], Literal["1", "2", "3", "4"]]

View File

@ -49,7 +49,7 @@ class BaseList(Component):
def create(
cls,
*children,
items: Optional[Union[Var[Iterable], Iterable]] = None,
items: Optional[Var[Iterable]] = None,
**props,
):
"""Create a list component.
@ -68,7 +68,7 @@ class BaseList(Component):
if isinstance(items, Var):
children = [Foreach.create(items, ListItem.create)]
else:
children = [ListItem.create(item) for item in items]
children = [ListItem.create(item) for item in items] # type: ignore
props["list_style_position"] = "outside"
props["direction"] = "column"
style = props.setdefault("style", {})

View File

@ -40,7 +40,7 @@ class BaseList(Component):
def create( # type: ignore
cls,
*children,
items: Optional[Union[Union[Var[Iterable], Iterable], Iterable]] = None,
items: Optional[Union[Var[Iterable], Iterable]] = None,
list_style_type: Optional[
Union[
Var[
@ -600,7 +600,7 @@ class List(ComponentNamespace):
@staticmethod
def __call__(
*children,
items: Optional[Union[Union[Var[Iterable], Iterable], Iterable]] = None,
items: Optional[Union[Var[Iterable], Iterable]] = None,
list_style_type: Optional[
Union[
Var[

View File

@ -0,0 +1,3 @@
"""Init file for the sonner component."""
from .toast import toast

View File

@ -0,0 +1,267 @@
"""Sonner toast component."""
from __future__ import annotations
from typing import Literal
from reflex.base import Base
from reflex.components.component import Component, ComponentNamespace
from reflex.components.lucide.icon import Icon
from reflex.event import EventSpec, call_script
from reflex.style import Style, color_mode
from reflex.utils import format
from reflex.utils.imports import ImportVar
from reflex.utils.serializers import serialize
from reflex.vars import Var, VarData
LiteralPosition = Literal[
"top-left",
"top-center",
"top-right",
"bottom-left",
"bottom-center",
"bottom-right",
]
toast_ref = Var.create_safe("refs['__toast']")
class PropsBase(Base):
"""Base class for all props classes."""
def json(self) -> str:
"""Convert the object to a json string.
Returns:
The object as a json string.
"""
from reflex.utils.serializers import serialize
return self.__config__.json_dumps(
{format.to_camel_case(key): value for key, value in self.dict().items()},
default=serialize,
)
class ToastProps(PropsBase):
"""Props for the toast component."""
# Toast's description, renders underneath the title.
description: str = ""
# Whether to show the close button.
close_button: bool = False
# Dark toast in light mode and vice versa.
invert: bool = False
# Control the sensitivity of the toast for screen readers
important: bool = False
# Time in milliseconds that should elapse before automatically closing the toast.
duration: int = 4000
# Position of the toast.
position: LiteralPosition = "bottom-right"
# If false, it'll prevent the user from dismissing the toast.
dismissible: bool = True
# 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
# TODO: fix implementation for action / cancel buttons
# Renders a primary button, clicking it will close the toast.
# action: str = ""
# Renders a secondary button, clicking it will close the toast.
# cancel: str = ""
# Custom id for the toast.
id: str = ""
# Removes the default styling, which allows for easier customization.
unstyled: bool = False
# Custom style for the toast.
style: Style = Style()
# Custom style for the toast primary button.
# action_button_styles: Style = Style()
# Custom style for the toast secondary button.
# cancel_button_styles: Style = Style()
class Toaster(Component):
"""A Toaster Component for displaying toast notifications."""
library = "sonner@1.4.41"
tag = "Toaster"
# the theme of the toast
theme: Var[str] = color_mode
# whether to show rich colors
rich_colors: Var[bool] = Var.create_safe(True)
# whether to expand the toast
expand: Var[bool] = Var.create_safe(True)
# the number of toasts that are currently visible
visible_toasts: Var[int]
# the position of the toast
position: Var[LiteralPosition] = Var.create_safe("bottom-right")
# whether to show the close button
close_button: Var[bool] = Var.create_safe(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]
def _get_hooks(self) -> Var[str]:
hook = Var.create_safe(f"{toast_ref} = toast", _var_is_local=True)
hook._var_data = VarData( # type: ignore
imports={
"/utils/state": [ImportVar(tag="refs")],
self.library: [ImportVar(tag="toast", install=False)],
}
)
return hook
@staticmethod
def send_toast(message: str, 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.
Returns:
The toast event.
"""
toast_command = f"{toast_ref}.{level}" if level is not None else toast_ref
if props:
args = serialize(ToastProps(**props))
toast = f"{toast_command}(`{message}`, {args})"
else:
toast = f"{toast_command}(`{message}`)"
toast_action = Var.create(toast, _var_is_string=False, _var_is_local=True)
return call_script(toast_action) # type: ignore
@staticmethod
def toast_info(message: str, **kwargs):
"""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, **kwargs):
"""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, **kwargs):
"""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, **kwargs):
"""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)
def toast_dismiss(self, id: str | None):
"""Dismiss a toast.
Args:
id: The id of the toast to dismiss.
Returns:
The toast dismiss event.
"""
if id is None:
dismiss = f"{toast_ref}.dismiss()"
else:
dismiss = f"{toast_ref}.dismiss({id})"
dismiss_action = Var.create(dismiss, _var_is_string=False, _var_is_local=True)
return call_script(dismiss_action) # type: ignore
# TODO: figure out why loading toast stay open forever
# def toast_loading(message: str, **kwargs):
# return _toast(message, level="loading", **kwargs)
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)
# loading = staticmethod(toast_loading)
__call__ = staticmethod(Toaster.send_toast)
toast = ToastNamespace()

View File

@ -0,0 +1,205 @@
"""Stub file for reflex/components/sonner/toast.py"""
# ------------------- DO NOT EDIT ----------------------
# This file was generated by `reflex/utils/pyi_generator.py`!
# ------------------------------------------------------
from typing import Any, Dict, Literal, Optional, Union, overload
from reflex.vars import Var, BaseVar, ComputedVar
from reflex.event import EventChain, EventHandler, EventSpec
from reflex.style import Style
from typing import Literal
from reflex.base import Base
from reflex.components.component import Component, ComponentNamespace
from reflex.components.lucide.icon import Icon
from reflex.event import EventSpec, call_script
from reflex.style import Style, color_mode
from reflex.utils import format
from reflex.utils.imports import ImportVar
from reflex.utils.serializers import serialize
from reflex.vars import Var, VarData
LiteralPosition = Literal[
"top-left",
"top-center",
"top-right",
"bottom-left",
"bottom-center",
"bottom-right",
]
toast_ref = Var.create_safe("refs['__toast']")
class PropsBase(Base):
def json(self) -> str: ...
class ToastProps(PropsBase):
description: str
close_button: bool
invert: bool
important: bool
duration: int
position: LiteralPosition
dismissible: bool
id: str
unstyled: bool
style: Style
class Toaster(Component):
@staticmethod
def send_toast(message: str, level: str | None = None, **props) -> EventSpec: ...
@staticmethod
def toast_info(message: str, **kwargs): ...
@staticmethod
def toast_warning(message: str, **kwargs): ...
@staticmethod
def toast_error(message: str, **kwargs): ...
@staticmethod
def toast_success(message: str, **kwargs): ...
def toast_dismiss(self, id: str | None): ...
@overload
@classmethod
def create( # type: ignore
cls,
*children,
theme: Optional[Union[Var[str], str]] = None,
rich_colors: Optional[Union[Var[bool], bool]] = None,
expand: Optional[Union[Var[bool], bool]] = None,
visible_toasts: Optional[Union[Var[int], int]] = None,
position: Optional[
Union[
Var[
Literal[
"top-left",
"top-center",
"top-right",
"bottom-left",
"bottom-center",
"bottom-right",
]
],
Literal[
"top-left",
"top-center",
"top-right",
"bottom-left",
"bottom-center",
"bottom-right",
],
]
] = None,
close_button: Optional[Union[Var[bool], bool]] = None,
offset: Optional[Union[Var[str], str]] = None,
dir: Optional[Union[Var[str], str]] = None,
hotkey: Optional[Union[Var[str], str]] = None,
invert: Optional[Union[Var[bool], bool]] = None,
toast_options: Optional[Union[Var[ToastProps], ToastProps]] = None,
gap: Optional[Union[Var[int], int]] = None,
loading_icon: Optional[Union[Var[Icon], Icon]] = None,
pause_when_page_is_hidden: Optional[Union[Var[bool], bool]] = None,
style: Optional[Style] = None,
key: Optional[Any] = None,
id: Optional[Any] = None,
class_name: Optional[Any] = None,
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_click: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_context_menu: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_double_click: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_focus: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mount: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mouse_down: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mouse_enter: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mouse_leave: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mouse_move: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mouse_out: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mouse_over: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mouse_up: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_scroll: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_unmount: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
**props
) -> "Toaster":
"""Create the component.
Args:
*children: The children of the component.
theme: the theme of the toast
rich_colors: whether to show rich colors
expand: whether to expand the toast
visible_toasts: the number of toasts that are currently visible
position: the position of the toast
close_button: whether to show the close button
offset: offset of the toast
dir: directionality of the toast (default: ltr)
hotkey: Keyboard shortcut that will move focus to the toaster area.
invert: Dark toasts in light mode and vice versa.
toast_options: These will act as default options for all toasts. See toast() for all available options.
gap: Gap between toasts when expanded
loading_icon: Changes the default loading icon
pause_when_page_is_hidden: Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked.
style: The style of the component.
key: A unique key for the component.
id: The id for the component.
class_name: The class name for the component.
autofocus: Whether the component should take the focus once the page is loaded
custom_attrs: custom attribute
**props: The props of the component.
Returns:
The component.
"""
...
class ToastNamespace(ComponentNamespace):
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)
@staticmethod
def __call__(message: str, level: Optional[str], **props) -> "Optional[EventSpec]":
"""Send a toast message.
Args:
message: The message to display.
level: The level of the toast.
**props: The options for the toast.
Returns:
The toast event.
"""
...
toast = ToastNamespace()

View File

@ -117,6 +117,29 @@ def _get_type_hint(value, type_hint_globals, is_optional=True) -> str:
"""
res = ""
args = get_args(value)
if value is type(None):
return "None"
if rx_types.is_union(value):
if type(None) in value.__args__:
res_args = [
_get_type_hint(arg, type_hint_globals, rx_types.is_optional(arg))
for arg in value.__args__
if arg is not type(None)
]
if len(res_args) == 1:
return f"Optional[{res_args[0]}]"
else:
res = f"Union[{', '.join(res_args)}]"
return f"Optional[{res}]"
res_args = [
_get_type_hint(arg, type_hint_globals, rx_types.is_optional(arg))
for arg in value.__args__
]
return f"Union[{', '.join(res_args)}]"
if args:
inner_container_type_args = (
[repr(arg) for arg in args]
@ -141,6 +164,20 @@ def _get_type_hint(value, type_hint_globals, is_optional=True) -> str:
res = f"Union[{res}]"
elif isinstance(value, str):
ev = eval(value, type_hint_globals)
if rx_types.is_optional(ev):
# hints = {
# _get_type_hint(arg, type_hint_globals, is_optional=False)
# for arg in ev.__args__
# }
return _get_type_hint(ev, type_hint_globals, is_optional=False)
# return f"Optional[{', '.join(hints)}]"
if rx_types.is_union(ev):
res = [
_get_type_hint(arg, type_hint_globals, rx_types.is_optional(arg))
for arg in ev.__args__
]
return f"Union[{', '.join(res)}]"
res = (
_get_type_hint(ev, type_hint_globals, is_optional=False)
if ev.__name__ == "Var"
@ -424,7 +461,58 @@ def _generate_component_create_functiondef(
return definition
def _generate_staticmethod_call_functiondef(
node: ast.FunctionDef | None,
clz: type[Component] | type[SimpleNamespace],
type_hint_globals: dict[str, Any],
) -> ast.FunctionDef | None:
...
fullspec = getfullargspec(clz.__call__)
call_args = ast.arguments(
args=[
ast.arg(
name,
annotation=ast.Name(
id=_get_type_hint(
anno := fullspec.annotations[name],
type_hint_globals,
is_optional=rx_types.is_optional(anno),
)
),
)
for name in fullspec.args
],
posonlyargs=[],
kwonlyargs=[],
kw_defaults=[],
kwarg=ast.arg(arg="props"),
defaults=[],
)
definition = ast.FunctionDef(
name="__call__",
args=call_args,
body=[
ast.Expr(value=ast.Constant(value=clz.__call__.__doc__)),
ast.Expr(
value=ast.Constant(...),
),
],
decorator_list=[ast.Name(id="staticmethod")],
lineno=node.lineno if node is not None else None,
returns=ast.Constant(
value=_get_type_hint(
typing.get_type_hints(clz.__call__).get("return", None),
type_hint_globals,
)
),
)
return definition
def _generate_namespace_call_functiondef(
node: ast.ClassDef | None,
clz_name: str,
classes: dict[str, type[Component] | type[SimpleNamespace]],
type_hint_globals: dict[str, Any],
@ -432,6 +520,7 @@ def _generate_namespace_call_functiondef(
"""Generate the __call__ function definition for a SimpleNamespace.
Args:
node: The existing __call__ classdef parent node from the ast
clz_name: The name of the SimpleNamespace class to generate the __call__ functiondef for.
classes: Map name to actual class definition.
type_hint_globals: The globals to use to resolving a type hint str.
@ -446,10 +535,12 @@ def _generate_namespace_call_functiondef(
clz = classes[clz_name]
if not hasattr(clz.__call__, "__self__"):
return _generate_staticmethod_call_functiondef(node, clz, type_hint_globals) # type: ignore
# Determine which class is wrapped by the namespace __call__ method
component_clz = clz.__call__.__self__
# Only generate for create functions
if clz.__call__.__func__.__name__ != "create":
return None
@ -603,6 +694,7 @@ class StubGenerator(ast.NodeTransformer):
if not child.targets[:]:
node.body.remove(child)
call_definition = _generate_namespace_call_functiondef(
node,
self.current_class,
self.classes,
type_hint_globals=self.type_hint_globals,

View File

@ -126,6 +126,18 @@ def is_generic_alias(cls: GenericType) -> bool:
return isinstance(cls, GenericAliasTypes)
def is_none(cls: GenericType) -> bool:
"""Check if a class is None.
Args:
cls: The class to check.
Returns:
Whether the class is None.
"""
return cls is type(None) or cls is None
def is_union(cls: GenericType) -> bool:
"""Check if a class is a Union.