accordion.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. """Radix accordion components."""
  2. from typing import Literal
  3. from reflex.components.component import Component
  4. from reflex.components.tags import Tag
  5. from reflex.style import Style
  6. from reflex.utils import format, imports
  7. from reflex.vars import Var
  8. LiteralAccordionType = Literal["single", "multiple"]
  9. LiteralAccordionDir = Literal["ltr", "rtl"]
  10. LiteralAccordionOrientation = Literal["vertical", "horizontal"]
  11. DEFAULT_ANIMATION_DURATION = 250
  12. class AccordionComponent(Component):
  13. """Base class for all @radix-ui/accordion components."""
  14. library: str = "@radix-ui/react-accordion@^1.1.2"
  15. # Change the default rendered element for the one passed as a child.
  16. as_child: Var[bool]
  17. def _render(self) -> Tag:
  18. return (
  19. super()
  20. ._render()
  21. .add_props(
  22. **{
  23. "class_name": format.to_title_case(self.tag or ""),
  24. }
  25. )
  26. )
  27. class AccordionRoot(AccordionComponent):
  28. """An accordion component."""
  29. tag: str = "Root"
  30. alias: str = "RadixAccordionRoot"
  31. # The type of accordion (single or multiple).
  32. type_: Var[LiteralAccordionType]
  33. # The value of the item to expand.
  34. value: Var[str]
  35. # The default value of the item to expand.
  36. default_value: Var[str]
  37. # Whether or not the accordion is collapsible.
  38. collapsible: Var[bool]
  39. # Whether or not the accordion is disabled.
  40. disabled: Var[bool]
  41. # The reading direction of the accordion when applicable.
  42. dir: Var[LiteralAccordionDir]
  43. # The orientation of the accordion.
  44. orientation: Var[LiteralAccordionOrientation]
  45. def _apply_theme(self, theme: Component):
  46. self.style = Style(
  47. {
  48. "border_radius": "6px",
  49. "background_color": "var(--accent-6)",
  50. "box_shadow": "0 2px 10px var(--black-a4)",
  51. **self.style,
  52. }
  53. )
  54. class AccordionItem(AccordionComponent):
  55. """An accordion component."""
  56. tag: str = "Item"
  57. alias: str = "RadixAccordionItem"
  58. # A unique identifier for the item.
  59. value: Var[str]
  60. # When true, prevents the user from interacting with the item.
  61. disabled: Var[bool]
  62. def _apply_theme(self, theme: Component):
  63. self.style = Style(
  64. {
  65. "overflow": "hidden",
  66. "margin_top": "1px",
  67. "&:first-child": {
  68. "margin_top": 0,
  69. "border_top_left_radius": "4px",
  70. "border_top_right_radius": "4px",
  71. },
  72. "&:last-child": {
  73. "border_bottom_left_radius": "4px",
  74. "border_bottom_right_radius": "4px",
  75. },
  76. "&:focus-within": {
  77. "position": "relative",
  78. "z_index": 1,
  79. "box_shadow": "0 0 0 2px var(--accent-7)",
  80. },
  81. **self.style,
  82. }
  83. )
  84. class AccordionHeader(AccordionComponent):
  85. """An accordion component."""
  86. tag: str = "Header"
  87. alias: str = "RadixAccordionHeader"
  88. def _apply_theme(self, theme: Component):
  89. self.style = Style(
  90. {
  91. "display": "flex",
  92. **self.style,
  93. }
  94. )
  95. class AccordionTrigger(AccordionComponent):
  96. """An accordion component."""
  97. tag: str = "Trigger"
  98. alias: str = "RadixAccordionTrigger"
  99. def _apply_theme(self, theme: Component):
  100. self.style = Style(
  101. {
  102. "font_family": "inherit",
  103. "padding": "0 20px",
  104. "height": "45px",
  105. "flex": 1,
  106. "display": "flex",
  107. "align_items": "center",
  108. "justify_content": "space-between",
  109. "font_size": "15px",
  110. "line_height": 1,
  111. "color": "var(--accent-11)",
  112. "box_shadow": "0 1px 0 var(--accent-6)",
  113. "&:hover": {
  114. "background_color": "var(--gray-2)",
  115. },
  116. "&[data-state='open'] > .AccordionChevron": {
  117. "transform": "rotate(180deg)",
  118. },
  119. **self.style,
  120. }
  121. )
  122. class AccordionContent(AccordionComponent):
  123. """An accordion component."""
  124. tag: str = "Content"
  125. alias: str = "RadixAccordionContent"
  126. def _apply_theme(self, theme: Component):
  127. self.style = Style(
  128. {
  129. "overflow": "hidden",
  130. "fontSize": "15px",
  131. "color": "var(--accent-11)",
  132. "backgroundColor": "var(--accent-2)",
  133. "padding": "15px, 20px",
  134. "&[data-state='open']": {
  135. "animation": Var.create(
  136. f"${{slideDown}} {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
  137. _var_is_string=True,
  138. ),
  139. },
  140. "&[data-state='closed']": {
  141. "animation": Var.create(
  142. f"${{slideUp}} {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
  143. _var_is_string=True,
  144. ),
  145. },
  146. **self.style,
  147. }
  148. )
  149. def _get_imports(self):
  150. return {
  151. **super()._get_imports(),
  152. "@emotion/react": [imports.ImportVar(tag="keyframes")],
  153. }
  154. def _get_custom_code(self) -> str:
  155. return """
  156. const slideDown = keyframes`
  157. from {
  158. height: 0;
  159. }
  160. to {
  161. height: var(--radix-accordion-content-height);
  162. }
  163. `
  164. const slideUp = keyframes`
  165. from {
  166. height: var(--radix-accordion-content-height);
  167. }
  168. to {
  169. height: 0;
  170. }
  171. `
  172. """
  173. # TODO: Remove this once the radix-icons PR is merged in.
  174. class ChevronDownIcon(Component):
  175. """A chevron down icon."""
  176. library: str = "@radix-ui/react-icons"
  177. tag: str = "ChevronDownIcon"
  178. def _apply_theme(self, theme: Component):
  179. self.style = Style(
  180. {
  181. "color": "var(--accent-10)",
  182. "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
  183. **self.style,
  184. }
  185. )
  186. accordion_root = AccordionRoot.create
  187. accordion_item = AccordionItem.create
  188. accordion_trigger = AccordionTrigger.create
  189. accordion_content = AccordionContent.create
  190. accordion_header = AccordionHeader.create
  191. chevron_down_icon = ChevronDownIcon.create
  192. def accordion(items: list[tuple[str, str]], **props) -> Component:
  193. """High level API for the Radix accordion.
  194. #TODO: We need to handle taking in state here. This is just for a POC.
  195. Args:
  196. items: The items of the accordion component: list of tuples (label,panel)
  197. **props: The properties of the component.
  198. Returns:
  199. The accordion component.
  200. """
  201. return accordion_root(
  202. *[
  203. accordion_item(
  204. accordion_header(
  205. accordion_trigger(
  206. label,
  207. chevron_down_icon(
  208. class_name="AccordionChevron",
  209. ),
  210. ),
  211. ),
  212. accordion_content(
  213. panel,
  214. ),
  215. value=f"item-{i}",
  216. )
  217. for i, (label, panel) in enumerate(items)
  218. ],
  219. **props,
  220. )