accordion.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. """Radix accordion components."""
  2. from __future__ import annotations
  3. from typing import Any, Dict, Literal
  4. from reflex.components.base.fragment import Fragment
  5. from reflex.components.component import Component
  6. from reflex.components.core import cond, match
  7. from reflex.components.radix.primitives.base import RadixPrimitiveComponent
  8. from reflex.components.radix.themes.components.icons import Icon
  9. from reflex.style import (
  10. Style,
  11. convert_dict_to_style_and_format_emotion,
  12. format_as_emotion,
  13. )
  14. from reflex.utils import imports
  15. from reflex.vars import BaseVar, Var
  16. LiteralAccordionType = Literal["single", "multiple"]
  17. LiteralAccordionDir = Literal["ltr", "rtl"]
  18. LiteralAccordionOrientation = Literal["vertical", "horizontal"]
  19. LiteralAccordionRootVariant = Literal["classic", "soft", "surface", "outline", "ghost"]
  20. LiteralAccordionRootColorScheme = Literal["primary", "accent"]
  21. DEFAULT_ANIMATION_DURATION = 250
  22. def get_theme_accordion_root(variant: Var[str], color_scheme: Var[str]) -> BaseVar:
  23. """Get the theme for the accordion root component.
  24. Args:
  25. variant: The variant of the accordion.
  26. color_scheme: The color of the accordion.
  27. Returns:
  28. The theme for the accordion root component.
  29. """
  30. return match( # type: ignore
  31. variant,
  32. (
  33. "soft",
  34. convert_dict_to_style_and_format_emotion(
  35. {
  36. "border_radius": "6px",
  37. "background_color": cond(
  38. color_scheme == "primary", "var(--accent-3)", "var(--slate-3)"
  39. ),
  40. "box_shadow": "0 2px 10px var(--black-a1)",
  41. }
  42. ),
  43. ),
  44. (
  45. "outline",
  46. convert_dict_to_style_and_format_emotion(
  47. {
  48. "border_radius": "6px",
  49. "border": cond(
  50. color_scheme == "primary",
  51. "1px solid var(--accent-6)",
  52. "1px solid var(--slate-6)",
  53. ),
  54. "box_shadow": "0 2px 10px var(--black-a1)",
  55. }
  56. ),
  57. ),
  58. (
  59. "surface",
  60. convert_dict_to_style_and_format_emotion(
  61. {
  62. "border_radius": "6px",
  63. "border": cond(
  64. color_scheme == "primary",
  65. "1px solid var(--accent-6)",
  66. "1px solid var(--slate-6)",
  67. ),
  68. "background_color": cond(
  69. color_scheme == "primary", "var(--accent-3)", "var(--slate-3)"
  70. ),
  71. "box_shadow": "0 2px 10px var(--black-a1)",
  72. }
  73. ),
  74. ),
  75. (
  76. "ghost",
  77. convert_dict_to_style_and_format_emotion(
  78. {
  79. "border_radius": "6px",
  80. "background_color": "none",
  81. "box_shadow": "None",
  82. }
  83. ),
  84. ),
  85. convert_dict_to_style_and_format_emotion(
  86. {
  87. "border_radius": "6px",
  88. "background_color": cond(
  89. color_scheme == "primary", "var(--accent-9)", "var(--slate-9)"
  90. ),
  91. "box_shadow": "0 2px 10px var(--black-a4)",
  92. }
  93. )
  94. # defaults to classic
  95. )
  96. def get_theme_accordion_item():
  97. """Get the theme for the accordion item component.
  98. Returns:
  99. The theme for the accordion item component.
  100. """
  101. return convert_dict_to_style_and_format_emotion(
  102. {
  103. "overflow": "hidden",
  104. "width": "100%",
  105. "margin_top": "1px",
  106. "&:first-child": {
  107. "margin_top": 0,
  108. "border_top_left_radius": "4px",
  109. "border_top_right_radius": "4px",
  110. },
  111. "&:last-child": {
  112. "border_bottom_left_radius": "4px",
  113. "border_bottom_right_radius": "4px",
  114. },
  115. "&:focus-within": {
  116. "position": "relative",
  117. "z_index": 1,
  118. },
  119. }
  120. )
  121. def get_theme_accordion_header() -> dict[str, str]:
  122. """Get the theme for the accordion header component.
  123. Returns:
  124. The theme for the accordion header component.
  125. """
  126. return {
  127. "display": "flex",
  128. }
  129. def get_theme_accordion_trigger(variant: str | Var, color_scheme: str | Var) -> BaseVar:
  130. """Get the theme for the accordion trigger component.
  131. Args:
  132. variant: The variant of the accordion.
  133. color_scheme: The color of the accordion.
  134. Returns:
  135. The theme for the accordion trigger component.
  136. """
  137. return match( # type: ignore
  138. variant,
  139. (
  140. "soft",
  141. convert_dict_to_style_and_format_emotion(
  142. {
  143. "color": cond(
  144. color_scheme == "primary",
  145. "var(--accent-11)",
  146. "var(--slate-11)",
  147. ),
  148. "&:hover": {
  149. "background_color": cond(
  150. color_scheme == "primary",
  151. "var(--accent-4)",
  152. "var(--slate-4)",
  153. ),
  154. },
  155. "& > .AccordionChevron": {
  156. "color": cond(
  157. color_scheme == "primary",
  158. "var(--accent-11)",
  159. "var(--slate-11)",
  160. ),
  161. "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
  162. },
  163. "&[data-state='open'] > .AccordionChevron": {
  164. "transform": "rotate(180deg)",
  165. },
  166. "font_family": "inherit",
  167. "width": "100%",
  168. "padding": "0 20px",
  169. "height": "45px",
  170. "flex": 1,
  171. "display": "flex",
  172. "align_items": "center",
  173. "justify_content": "space-between",
  174. "font_size": "15px",
  175. "line_height": 1,
  176. }
  177. ),
  178. ),
  179. (
  180. "outline",
  181. "surface",
  182. "ghost",
  183. convert_dict_to_style_and_format_emotion(
  184. {
  185. "color": cond(
  186. color_scheme == "primary",
  187. "var(--accent-11)",
  188. "var(--slate-11)",
  189. ),
  190. "&:hover": {
  191. "background_color": cond(
  192. color_scheme == "primary",
  193. "var(--accent-4)",
  194. "var(--slate-4)",
  195. ),
  196. },
  197. "& > .AccordionChevron": {
  198. "color": cond(
  199. color_scheme == "primary",
  200. "var(--accent-11)",
  201. "var(--slate-11)",
  202. ),
  203. "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
  204. },
  205. "&[data-state='open'] > .AccordionChevron": {
  206. "transform": "rotate(180deg)",
  207. },
  208. "font_family": "inherit",
  209. "width": "100%",
  210. "padding": "0 20px",
  211. "height": "45px",
  212. "flex": 1,
  213. "display": "flex",
  214. "align_items": "center",
  215. "justify_content": "space-between",
  216. "font_size": "15px",
  217. "line_height": 1,
  218. }
  219. ),
  220. ),
  221. # defaults to classic
  222. convert_dict_to_style_and_format_emotion(
  223. {
  224. "color": cond(
  225. color_scheme == "primary",
  226. "var(--accent-9-contrast)",
  227. "var(--slate-9-contrast)",
  228. ),
  229. "box_shadow": cond(
  230. color_scheme == "primary",
  231. "0 1px 0 var(--accent-6)",
  232. "0 1px 0 var(--slate-11)",
  233. ),
  234. "&:hover": {
  235. "background_color": cond(
  236. color_scheme == "primary", "var(--accent-10)", "var(--slate-10)"
  237. ),
  238. },
  239. "& > .AccordionChevron": {
  240. "color": cond(
  241. color_scheme == "primary",
  242. "var(--accent-9-contrast)",
  243. "var(--slate-9-contrast)",
  244. ),
  245. "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
  246. },
  247. "&[data-state='open'] > .AccordionChevron": {
  248. "transform": "rotate(180deg)",
  249. },
  250. "font_family": "inherit",
  251. "width": "100%",
  252. "padding": "0 20px",
  253. "height": "45px",
  254. "flex": 1,
  255. "display": "flex",
  256. "align_items": "center",
  257. "justify_content": "space-between",
  258. "font_size": "15px",
  259. "line_height": 1,
  260. }
  261. ),
  262. )
  263. def get_theme_accordion_content(variant: str | Var, color_scheme: str | Var) -> BaseVar:
  264. """Get the theme for the accordion content component.
  265. Args:
  266. variant: The variant of the accordion.
  267. color_scheme: The color of the accordion.
  268. Returns:
  269. The theme for the accordion content component.
  270. """
  271. return match( # type: ignore
  272. variant,
  273. (
  274. "outline",
  275. "ghost",
  276. convert_dict_to_style_and_format_emotion(
  277. {
  278. "overflow": "hidden",
  279. "font_size": "10px",
  280. "color": cond(
  281. color_scheme == "primary",
  282. "var(--accent-11)",
  283. "var(--slate-11)",
  284. ),
  285. "padding": "15px, 20px",
  286. "&[data-state='open']": {
  287. "animation": Var.create(
  288. f"${{slideDown}} {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
  289. _var_is_string=True,
  290. ),
  291. },
  292. "&[data-state='closed']": {
  293. "animation": Var.create(
  294. f"${{slideUp}} {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
  295. _var_is_string=True,
  296. ),
  297. },
  298. }
  299. ),
  300. ),
  301. convert_dict_to_style_and_format_emotion(
  302. {
  303. "overflow": "hidden",
  304. "font_size": "10px",
  305. "color": match(
  306. variant,
  307. (
  308. "classic",
  309. cond(
  310. color_scheme == "primary",
  311. "var(--accent-9-contrast)",
  312. "var(--slate-9-contrast)",
  313. ),
  314. ),
  315. cond(
  316. color_scheme == "primary", "var(--accent-11)", "var(--slate-11)"
  317. ),
  318. ),
  319. "background_color": match(
  320. variant,
  321. (
  322. "classic",
  323. cond(
  324. color_scheme == "primary",
  325. "var(--accent-9)",
  326. "var(--slate-9)",
  327. ),
  328. ),
  329. cond(
  330. color_scheme == "primary", "var(--accent-3)", "var(--slate-3)"
  331. ),
  332. ),
  333. "padding": "15px, 20px",
  334. "&[data-state='open']": {
  335. "animation": Var.create(
  336. f"${{slideDown}} {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
  337. _var_is_string=True,
  338. ),
  339. },
  340. "&[data-state='closed']": {
  341. "animation": Var.create(
  342. f"${{slideUp}} {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
  343. _var_is_string=True,
  344. ),
  345. },
  346. }
  347. ),
  348. )
  349. class AccordionComponent(RadixPrimitiveComponent):
  350. """Base class for all @radix-ui/accordion components."""
  351. library = "@radix-ui/react-accordion@^1.1.2"
  352. class AccordionRoot(AccordionComponent):
  353. """An accordion component."""
  354. tag = "Root"
  355. alias = "RadixAccordionRoot"
  356. # The type of accordion (single or multiple).
  357. type_: Var[LiteralAccordionType]
  358. # The value of the item to expand.
  359. value: Var[str]
  360. # The default value of the item to expand.
  361. default_value: Var[str]
  362. # Whether or not the accordion is collapsible.
  363. collapsible: Var[bool]
  364. # Whether or not the accordion is disabled.
  365. disabled: Var[bool]
  366. # The reading direction of the accordion when applicable.
  367. dir: Var[LiteralAccordionDir]
  368. # The orientation of the accordion.
  369. orientation: Var[LiteralAccordionOrientation]
  370. # The variant of the accordion.
  371. variant: Var[LiteralAccordionRootVariant] = "classic" # type: ignore
  372. # The color scheme of the accordion.
  373. color_scheme: Var[LiteralAccordionRootColorScheme] = "primary" # type: ignore
  374. # dynamic themes of the accordion generated at compile time.
  375. _dynamic_themes: Var[dict]
  376. @classmethod
  377. def create(cls, *children, **props) -> Component:
  378. """Create the Accordion root component.
  379. Args:
  380. *children: The children of the component.
  381. **props: The properties of the component.
  382. Returns:
  383. The Accordion root Component.
  384. """
  385. comp = super().create(*children, **props)
  386. if not comp.color_scheme._var_state: # type: ignore
  387. # mark the vars of color string literals as strings so they can be formatted properly when performing a var operation.
  388. comp.color_scheme._var_is_string = True # type: ignore
  389. if not comp.variant._var_state: # type: ignore
  390. # mark the vars of variant string literals as strings so they are formatted properly in the match condition.
  391. comp.variant._var_is_string = True # type: ignore
  392. # remove Fragment and cond wrap workaround when https://github.com/reflex-dev/reflex/issues/2393 is resolved.
  393. return Fragment.create(comp, cond(True, Fragment.create()))
  394. def _get_style(self) -> dict:
  395. """Get the style for the component.
  396. Returns:
  397. The dictionary of the component style as value and the style notation as key.
  398. """
  399. return {"css": self._dynamic_themes._merge(format_as_emotion(self.style))} # type: ignore
  400. def _apply_theme(self, theme: Component):
  401. self._dynamic_themes = Var.create( # type: ignore
  402. convert_dict_to_style_and_format_emotion(
  403. {
  404. "& .AccordionItem": get_theme_accordion_item(),
  405. "& .AccordionHeader": get_theme_accordion_header(),
  406. "& .AccordionTrigger": get_theme_accordion_trigger(
  407. variant=self.variant, color_scheme=self.color_scheme
  408. ),
  409. "& .AccordionContent": get_theme_accordion_content(
  410. variant=self.variant, color_scheme=self.color_scheme
  411. ),
  412. }
  413. )
  414. )._merge( # type: ignore
  415. get_theme_accordion_root(
  416. variant=self.variant, color_scheme=self.color_scheme
  417. )
  418. )
  419. def get_event_triggers(self) -> Dict[str, Any]:
  420. """Get the events triggers signatures for the component.
  421. Returns:
  422. The signatures of the event triggers.
  423. """
  424. return {
  425. **super().get_event_triggers(),
  426. "on_value_change": lambda e0: [e0],
  427. }
  428. class AccordionItem(AccordionComponent):
  429. """An accordion component."""
  430. tag = "Item"
  431. alias = "RadixAccordionItem"
  432. # A unique identifier for the item.
  433. value: Var[str]
  434. # When true, prevents the user from interacting with the item.
  435. disabled: Var[bool]
  436. def _apply_theme(self, theme: Component):
  437. self.style = Style(
  438. {
  439. **self.style,
  440. }
  441. )
  442. class AccordionHeader(AccordionComponent):
  443. """An accordion component."""
  444. tag = "Header"
  445. alias = "RadixAccordionHeader"
  446. def _apply_theme(self, theme: Component):
  447. self.style = Style(
  448. {
  449. **self.style,
  450. }
  451. )
  452. class AccordionTrigger(AccordionComponent):
  453. """An accordion component."""
  454. tag = "Trigger"
  455. alias = "RadixAccordionTrigger"
  456. def _apply_theme(self, theme: Component):
  457. self.style = Style(
  458. {
  459. **self.style,
  460. }
  461. )
  462. class AccordionContent(AccordionComponent):
  463. """An accordion component."""
  464. tag = "Content"
  465. alias = "RadixAccordionContent"
  466. def _apply_theme(self, theme: Component):
  467. self.style = Style(
  468. {
  469. **self.style,
  470. }
  471. )
  472. def _get_imports(self):
  473. return {
  474. **super()._get_imports(),
  475. "@emotion/react": [imports.ImportVar(tag="keyframes")],
  476. }
  477. def _get_custom_code(self) -> str:
  478. return """
  479. const slideDown = keyframes`
  480. from {
  481. height: 0;
  482. }
  483. to {
  484. height: var(--radix-accordion-content-height);
  485. }
  486. `
  487. const slideUp = keyframes`
  488. from {
  489. height: var(--radix-accordion-content-height);
  490. }
  491. to {
  492. height: 0;
  493. }
  494. `
  495. """
  496. def accordion_item(header: Component, content: Component, **props) -> Component:
  497. """Create an accordion item.
  498. Args:
  499. header: The header of the accordion item.
  500. content: The content of the accordion item.
  501. **props: Additional properties to apply to the accordion item.
  502. Returns:
  503. The accordion item.
  504. """
  505. # The item requires a value to toggle (use the header as the default value).
  506. value = props.pop("value", str(header))
  507. return AccordionItem.create(
  508. AccordionHeader.create(
  509. AccordionTrigger.create(
  510. header,
  511. Icon.create(
  512. tag="chevron_down",
  513. class_name="AccordionChevron",
  514. ),
  515. class_name="AccordionTrigger",
  516. ),
  517. ),
  518. AccordionContent.create(
  519. content,
  520. class_name="AccordionContent",
  521. ),
  522. value=value,
  523. **props,
  524. class_name="AccordionItem",
  525. )