Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Radix primitive based Drawer component #2445

Merged
merged 9 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions reflex/components/radix/primitives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
AccordionTrigger,
accordion_item,
)
from .drawer import (
drawer_close,
drawer_content,
drawer_description,
drawer_overlay,
drawer_portal,
drawer_root,
drawer_title,
drawer_trigger,
)
from .form import (
form_control,
form_field,
Expand Down
240 changes: 240 additions & 0 deletions reflex/components/radix/primitives/drawer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
"""Drawer components based on Radix primitives."""
# Based on Vaul: https://github.com/emilkowalski/vaul
# Style based on https://ui.shadcn.com/docs/components/drawer
from __future__ import annotations

from typing import Any, Dict, List, Literal, Optional, Union

from reflex.components.radix.primitives.base import RadixPrimitiveComponentWithClassName
from reflex.constants import EventTriggers
from reflex.vars import Var


class DrawerComponent(RadixPrimitiveComponentWithClassName):
"""A Drawer component."""

library = "vaul"

lib_dependencies: List[str] = ["@radix-ui/react-dialog@^1.0.5"]


LiteralDirectionType = Literal[
"top",
"bottom",
"left",
"right",
]


class DrawerRoot(DrawerComponent):
"""The Root component of a Drawer, contains all parts of a drawer."""

tag = "Drawer.Root"

# Whether the drawer is open or not.
open: Var[bool]

# Enable background scaling,
# it requires an element with [vaul-drawer-wrapper] data attribute to scale its background.
should_scale_background: Var[bool]

# Number between 0 and 1 that determines when the drawer should be closed.
close_threshold: Var[float]

# Array of numbers from 0 to 100 that corresponds to % of the screen a given snap point should take up. Should go from least visible.
# Also Accept px values, which doesn't take screen height into account.
snap_points: Optional[List[Union[str, float]]]

# Index of a snapPoint from which the overlay fade should be applied.
# Defaults to the last snap point.
# TODO: will it accept -1 then?
fade_from_index: Var[int]

# Duration for which the drawer is not draggable after scrolling content inside of the drawer. Defaults to 500ms
scroll_lock_timeout: Var[int]

# When `False`, it allows to interact with elements outside of the drawer without closing it.
# Defaults to `True`.
modal: Var[bool]

# Direction of the drawer. Defaults to `"bottom"`
direction: Var[LiteralDirectionType]

# When `True`, it prevents scroll restoration
# when the drawer is closed after a navigation happens inside of it.
# Defaults to `True`.
preventScrollRestoration: Var[bool]

def get_event_triggers(self) -> Dict[str, Any]:
"""Get the event triggers that pass the component's value to the handler.

Returns:
A dict mapping the event trigger to the var that is passed to the handler.
"""
return {
**super().get_event_triggers(),
EventTriggers.ON_OPEN_CHANGE: lambda e0: [e0.target.value],
}


class DrawerTrigger(DrawerComponent):
"""The button that opens the dialog."""

tag = "Drawer.Trigger"

as_child: Var[bool]


class DrawerPortal(DrawerComponent):
"""Portals your drawer into the body."""

tag = "Drawer.Portal"


# Based on https://www.radix-ui.com/primitives/docs/components/dialog#content
class DrawerContent(DrawerComponent):
"""Content that should be rendered in the drawer."""

tag = "Drawer.Content"

# Style set partially based on the source code at https://ui.shadcn.com/docs/components/drawer
def _get_style(self) -> dict:
"""Get the style for the component.

Returns:
The dictionary of the component style as value and the style notation as key.
"""
base_style = {
"left": "0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be setting all of these? Then it will stretch to the full width of the page I believe

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A variation of that if not that precisely. Basically trying to replicate the shadcn UI implementation, their drawer is basically this drawer component plus this. From the look of it, their drawer is always coming up from bottom. Without these style below, the drawer will be just a dialogue flying in from a certain direction (not sure if that should still be called drawer).

<DrawerPrimitive.Content
      ref={ref}
      className={cn(
        "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
        className
      )}
      {...props}
    >

"right": "0",
"bottom": "0",
"top": "0",
"position": "fixed",
"z_index": 50,
"display": "flex",
}
style = self.style or {}
base_style.update(style)
self.style.update(
{
"css": base_style,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you have to put it in the css tag? I think we automatically handle this now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must've been misinformed. Can you point me to an example how the latest way to do this?

}
)
return self.style

def get_event_triggers(self) -> Dict[str, Any]:
"""Get the events triggers signatures for the component.

Returns:
The signatures of the event triggers.
"""
return {
**super().get_event_triggers(),
# DrawerContent is based on Radix DialogContent
# These are the same triggers as DialogContent
EventTriggers.ON_OPEN_AUTO_FOCUS: lambda e0: [e0.target.value],
EventTriggers.ON_CLOSE_AUTO_FOCUS: lambda e0: [e0.target.value],
EventTriggers.ON_ESCAPE_KEY_DOWN: lambda e0: [e0.target.value],
EventTriggers.ON_POINTER_DOWN_OUTSIDE: lambda e0: [e0.target.value],
EventTriggers.ON_INTERACT_OUTSIDE: lambda e0: [e0.target.value],
}


class DrawerOverlay(DrawerComponent):
"""A layer that covers the inert portion of the view when the dialog is open."""

tag = "Drawer.Overlay"

# Style set based on the source code at https://ui.shadcn.com/docs/components/drawer
def _get_style(self) -> dict:
"""Get the style for the component.

Returns:
The dictionary of the component style as value and the style notation as key.
"""
base_style = {
"position": "fixed",
"left": "0",
"right": "0",
"bottom": "0",
"top": "0",
"z_index": 50,
"background": "rgba(0, 0, 0, 0.8)",
}
style = self.style or {}
base_style.update(style)
self.style.update(
{
"css": base_style,
}
)
return self.style


class DrawerClose(DrawerComponent):
"""A button that closes the drawer."""

tag = "Drawer.Close"


class DrawerTitle(DrawerComponent):
"""A title for the drawer."""

tag = "Drawer.Title"

# Style set based on the source code at https://ui.shadcn.com/docs/components/drawer
def _get_style(self) -> dict:
"""Get the style for the component.

Returns:
The dictionary of the component style as value and the style notation as key.
"""
base_style = {
"font-size": "1.125rem",
"font-weight": "600",
"line-weight": "1",
"letter-spacing": "-0.05em",
}
style = self.style or {}
base_style.update(style)
self.style.update(
{
"css": base_style,
}
)
return self.style


class DrawerDescription(DrawerComponent):
"""A description for the drawer."""

tag = "Drawer.Description"

# Style set based on the source code at https://ui.shadcn.com/docs/components/drawer
def _get_style(self) -> dict:
"""Get the style for the component.

Returns:
The dictionary of the component style as value and the style notation as key.
"""
base_style = {
"font-size": "0.875rem",
}
style = self.style or {}
base_style.update(style)
self.style.update(
{
"css": base_style,
}
)
return self.style


drawer_root = DrawerRoot.create
drawer_trigger = DrawerTrigger.create
drawer_portal = DrawerPortal.create
drawer_content = DrawerContent.create
drawer_overlay = DrawerOverlay.create
drawer_close = DrawerClose.create
drawer_title = DrawerTitle.create
drawer_description = DrawerDescription.create