accordion.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. """Radix accordion components."""
  2. from __future__ import annotations
  3. from typing import Any, Dict, List, Literal, Optional, Union
  4. from reflex.components.component import Component, ComponentNamespace
  5. from reflex.components.core.colors import color
  6. from reflex.components.lucide.icon import Icon
  7. from reflex.components.radix.primitives.base import RadixPrimitiveComponent
  8. from reflex.components.radix.themes.base import LiteralAccentColor
  9. from reflex.style import Style
  10. from reflex.utils import imports
  11. from reflex.vars import Var, get_uuid_string_var
  12. LiteralAccordionType = Literal["single", "multiple"]
  13. LiteralAccordionDir = Literal["ltr", "rtl"]
  14. LiteralAccordionOrientation = Literal["vertical", "horizontal"]
  15. LiteralAccordionVariant = Literal["classic", "soft", "surface", "outline", "ghost"]
  16. DEFAULT_ANIMATION_DURATION = 250
  17. class AccordionComponent(RadixPrimitiveComponent):
  18. """Base class for all @radix-ui/accordion components."""
  19. library = "@radix-ui/react-accordion@^1.1.2"
  20. # The color scheme of the component.
  21. color_scheme: Var[LiteralAccentColor]
  22. # The variant of the component.
  23. variant: Var[LiteralAccordionVariant] = Var.create_safe("classic")
  24. def add_style(self) -> Style | None:
  25. """Add style to the component."""
  26. if self.color_scheme is not None:
  27. self.custom_attrs["data-accent-color"] = self.color_scheme
  28. self.custom_attrs["data-variant"] = self.variant
  29. def _exclude_props(self) -> list[str]:
  30. return ["color_scheme", "variant"]
  31. class AccordionRoot(AccordionComponent):
  32. """An accordion component."""
  33. tag = "Root"
  34. alias = "RadixAccordionRoot"
  35. # The type of accordion (single or multiple).
  36. type: Var[LiteralAccordionType]
  37. # The value of the item to expand.
  38. value: Var[Union[str, List[str]]]
  39. # The default value of the item to expand.
  40. default_value: Var[Union[str, List[str]]]
  41. # Whether or not the accordion is collapsible.
  42. collapsible: Var[bool]
  43. # Whether or not the accordion is disabled.
  44. disabled: Var[bool]
  45. # The reading direction of the accordion when applicable.
  46. dir: Var[LiteralAccordionDir]
  47. # The orientation of the accordion.
  48. orientation: Var[LiteralAccordionOrientation]
  49. # The variant of the accordion.
  50. variant: Var[LiteralAccordionVariant] = Var.create_safe("classic")
  51. _valid_children: List[str] = ["AccordionItem"]
  52. @classmethod
  53. def create(cls, *children, **props) -> Component:
  54. """Create the Accordion root component.
  55. Args:
  56. *children: The children of the component.
  57. **props: The properties of the component.
  58. Returns:
  59. The Accordion root Component.
  60. """
  61. for child in children:
  62. if isinstance(child, AccordionItem):
  63. child.color_scheme = props.get("color_scheme") # type: ignore
  64. child.variant = props.get("variant") # type: ignore
  65. return super().create(*children, **props)
  66. def get_event_triggers(self) -> Dict[str, Any]:
  67. """Get the events triggers signatures for the component.
  68. Returns:
  69. The signatures of the event triggers.
  70. """
  71. return {
  72. **super().get_event_triggers(),
  73. "on_value_change": lambda e0: [e0],
  74. }
  75. def add_style(self):
  76. """Add style to the component.
  77. Returns:
  78. The style of the component.
  79. """
  80. return Style(
  81. {
  82. "border_radius": "6px",
  83. "box_shadow": f"0 2px 10px {color('black', 1, alpha=True)}",
  84. "&[data-variant='classic']": {
  85. "background_color": color("accent", 9),
  86. "box_shadow": f"0 2px 10px {color('black', 4, alpha=True)}",
  87. },
  88. "&[data-variant='soft']": {
  89. "background_color": color("accent", 3),
  90. },
  91. "&[data-variant='outline']": {
  92. "border": f"1px solid {color('accent', 6)}",
  93. },
  94. "&[data-variant='surface']": {
  95. "border": f"1px solid {color('accent', 6)}",
  96. "background_color": color("accent", 3),
  97. },
  98. "&[data-variant='ghost']": {
  99. "background_color": "none",
  100. "box_shadow": "None",
  101. },
  102. }
  103. )
  104. class AccordionItem(AccordionComponent):
  105. """An accordion component."""
  106. tag = "Item"
  107. alias = "RadixAccordionItem"
  108. # A unique identifier for the item.
  109. value: Var[str]
  110. # When true, prevents the user from interacting with the item.
  111. disabled: Var[bool]
  112. _valid_children: List[str] = [
  113. "AccordionHeader",
  114. "AccordionTrigger",
  115. "AccordionContent",
  116. ]
  117. _valid_parents: List[str] = ["AccordionRoot"]
  118. @classmethod
  119. def create(
  120. cls,
  121. *children,
  122. header: Optional[Component | Var] = None,
  123. content: Optional[Component | Var] = None,
  124. **props,
  125. ) -> Component:
  126. """Create an accordion item.
  127. Args:
  128. *children: The list of children to use if header and content are not provided.
  129. header: The header of the accordion item.
  130. content: The content of the accordion item.
  131. **props: Additional properties to apply to the accordion item.
  132. Returns:
  133. The accordion item.
  134. """
  135. # The item requires a value to toggle (use a random unique name if not provided).
  136. value = props.pop("value", get_uuid_string_var())
  137. if "AccordionItem" not in (
  138. cls_name := props.pop("class_name", "AccordionItem")
  139. ):
  140. cls_name = f"{cls_name} AccordionItem"
  141. if (header is not None) and (content is not None):
  142. children = [
  143. AccordionHeader.create(
  144. AccordionTrigger.create(
  145. header,
  146. AccordionIcon.create(
  147. color_scheme=props.get("color_scheme"),
  148. variant=props.get("variant"),
  149. ),
  150. color_scheme=props.get("color_scheme"),
  151. variant=props.get("variant"),
  152. ),
  153. color_scheme=props.get("color_scheme"),
  154. variant=props.get("variant"),
  155. ),
  156. AccordionContent.create(
  157. content, color_scheme=props.get("color_scheme")
  158. ),
  159. ]
  160. return super().create(*children, value=value, **props, class_name=cls_name)
  161. def add_style(self) -> Style | None:
  162. """Add style to the component.
  163. Returns:
  164. The style of the component.
  165. """
  166. for child in self.children:
  167. if isinstance(child, (AccordionHeader, AccordionContent)):
  168. child.color_scheme = self.color_scheme
  169. child.variant = self.variant
  170. return Style(
  171. {
  172. "overflow": "hidden",
  173. "width": "100%",
  174. "margin_top": "1px",
  175. "&:first-child": {
  176. "margin_top": 0,
  177. "border_top_left_radius": "4px",
  178. "border_top_right_radius": "4px",
  179. },
  180. "&:last-child": {
  181. "border_bottom_left_radius": "4px",
  182. "border_bottom_right_radius": "4px",
  183. },
  184. "&:focus-within": {
  185. "position": "relative",
  186. "z_index": 1,
  187. },
  188. }
  189. )
  190. class AccordionHeader(AccordionComponent):
  191. """An accordion component."""
  192. tag = "Header"
  193. alias = "RadixAccordionHeader"
  194. @classmethod
  195. def create(cls, *children, **props) -> Component:
  196. """Create the Accordion header component.
  197. Args:
  198. *children: The children of the component.
  199. **props: The properties of the component.
  200. Returns:
  201. The Accordion header Component.
  202. """
  203. if "AccordionHeader" not in (
  204. cls_name := props.pop("class_name", "AccordionHeader")
  205. ):
  206. cls_name = f"{cls_name} AccordionHeader"
  207. return super().create(*children, class_name=cls_name, **props)
  208. def add_style(self) -> Style | None:
  209. """Add style to the component.
  210. Returns:
  211. The style of the component.
  212. """
  213. for child in self.children:
  214. if isinstance(child, AccordionTrigger):
  215. child.color_scheme = self.color_scheme
  216. child.variant = self.variant
  217. return Style({"display": "flex"})
  218. cubic_bezier = "cubic-bezier(0.87, 0, 0.13, 1)"
  219. class AccordionTrigger(AccordionComponent):
  220. """An accordion component."""
  221. tag = "Trigger"
  222. alias = "RadixAccordionTrigger"
  223. @classmethod
  224. def create(cls, *children, **props) -> Component:
  225. """Create the Accordion trigger component.
  226. Args:
  227. *children: The children of the component.
  228. **props: The properties of the component.
  229. Returns:
  230. The Accordion trigger Component.
  231. """
  232. if "AccordionTrigger" not in (
  233. cls_name := props.pop("class_name", "AccordionTrigger")
  234. ):
  235. cls_name = f"{cls_name} AccordionTrigger"
  236. return super().create(*children, class_name=cls_name, **props)
  237. def add_style(self) -> Style | None:
  238. """Add style to the component.
  239. Returns:
  240. The style of the component.
  241. """
  242. for child in self.children:
  243. if isinstance(child, AccordionIcon):
  244. child.color_scheme = self.color_scheme
  245. child.variant = self.variant
  246. return Style(
  247. {
  248. "color": color("accent", 11),
  249. "line_height": 1,
  250. "font_size": "15px",
  251. "justify_content": "space-between",
  252. "align_items": "center",
  253. "flex": 1,
  254. "display": "flex",
  255. "padding": "0 20px",
  256. "height": "45px",
  257. "font_family": "inherit",
  258. "width": "100%",
  259. "&[data-state='open'] > .AccordionChevron": {
  260. "transform": "rotate(180deg)",
  261. },
  262. "&:hover": {
  263. "background_color": color("accent", 4),
  264. },
  265. "& > .AccordionChevron": {
  266. "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms {cubic_bezier}",
  267. },
  268. "&[data-variant='classic']": {
  269. "color": color("accent", 12),
  270. "box_shadow": color("accent", 11),
  271. "&:hover": {
  272. "background_color": color("accent", 10),
  273. },
  274. "& > .AccordionChevron": {
  275. "color": color("accent", 12),
  276. "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms {cubic_bezier}",
  277. },
  278. },
  279. }
  280. )
  281. class AccordionIcon(Icon):
  282. """An accordion icon component."""
  283. @classmethod
  284. def create(cls, *children, **props) -> Component:
  285. """Create the Accordion icon component.
  286. Args:
  287. *children: The children of the component.
  288. **props: The properties of the component.
  289. Returns:
  290. The Accordion icon Component.
  291. """
  292. if "AccordionChevron" not in (
  293. cls_name := props.pop("class_name", "AccordionChevron")
  294. ):
  295. cls_name = f"{cls_name} AccordionChevron"
  296. return super().create(tag="chevron_down", class_name=cls_name, **props)
  297. class AccordionContent(AccordionComponent):
  298. """An accordion component."""
  299. tag = "Content"
  300. alias = "RadixAccordionContent"
  301. def add_imports(self) -> imports.ImportDict:
  302. """Add imports to the component.
  303. Returns:
  304. The imports of the component.
  305. """
  306. return {"@emotion/react": [imports.ImportVar(tag="keyframes")]}
  307. @classmethod
  308. def create(cls, *children, **props) -> Component:
  309. """Create the Accordion content component.
  310. Args:
  311. *children: The children of the component.
  312. **props: The properties of the component.
  313. Returns:
  314. The Accordion content Component.
  315. """
  316. if "AccordionContent" not in (
  317. cls_name := props.pop("class_name", "AccordionContent")
  318. ):
  319. cls_name = f"{cls_name} AccordionContent"
  320. return super().create(*children, class_name=cls_name, **props)
  321. def add_custom_code(self) -> list[str]:
  322. """Add custom code to the component.
  323. Returns:
  324. The custom code of the component.
  325. """
  326. return [
  327. """
  328. const slideDown = keyframes`
  329. from {
  330. height: 0;
  331. }
  332. to {
  333. height: var(--radix-accordion-content-height);
  334. }
  335. `
  336. const slideUp = keyframes`
  337. from {
  338. height: var(--radix-accordion-content-height);
  339. }
  340. to {
  341. height: 0;
  342. }
  343. `
  344. """
  345. ]
  346. def add_style(self) -> Style | None:
  347. """Add style to the component.
  348. Returns:
  349. The style of the component.
  350. """
  351. slideDown = Var.create(
  352. f"${{slideDown}} {DEFAULT_ANIMATION_DURATION}ms {cubic_bezier}",
  353. _var_is_string=True,
  354. )
  355. slideUp = Var.create(
  356. f"${{slideUp}} {DEFAULT_ANIMATION_DURATION}ms {cubic_bezier}",
  357. _var_is_string=True,
  358. )
  359. return Style(
  360. {
  361. "overflow": "hidden",
  362. "font_size": "10px",
  363. "color": color("accent", 11),
  364. "background_color": color("accent", 3),
  365. "padding": "0 15px",
  366. "&[data-state='open']": {"animation": slideDown},
  367. "&[data-state='closed']": {"animation": slideUp},
  368. "&[data-variant='classic']": {
  369. "color": color("accent", 12),
  370. "background_color": color("accent", 9),
  371. },
  372. "&[data-variant='outline']": {"background_color": "transparent"},
  373. "&[data-variant='ghost']": {"background_color": "transparent"},
  374. }
  375. )
  376. class Accordion(ComponentNamespace):
  377. """Accordion component."""
  378. content = staticmethod(AccordionContent.create)
  379. header = staticmethod(AccordionHeader.create)
  380. item = staticmethod(AccordionItem.create)
  381. icon = staticmethod(AccordionIcon.create)
  382. root = staticmethod(AccordionRoot.create)
  383. trigger = staticmethod(AccordionTrigger.create)
  384. accordion = Accordion()