|
| 1 | +"""Custom tabs component.""" |
| 2 | + |
| 3 | +from typing import Literal |
| 4 | + |
| 5 | +from reflex.components.component import Component, ComponentNamespace |
| 6 | +from reflex.event import EventHandler, passthrough_event_spec |
| 7 | +from reflex.utils.imports import ImportVar |
| 8 | +from reflex.vars import Var |
| 9 | + |
| 10 | +from reflex_ui.components.base_ui import PACKAGE_NAME, BaseUIComponent |
| 11 | + |
| 12 | +LiteralOrientation = Literal["horizontal", "vertical"] |
| 13 | + |
| 14 | + |
| 15 | +class ClassNames: |
| 16 | + """Class names for tabs components.""" |
| 17 | + |
| 18 | + ROOT = "flex flex-col gap-2" |
| 19 | + LIST = "bg-secondary-3 inline-flex gap-1 p-1 items-center justify-start rounded-md relative z-0" |
| 20 | + TAB = "h-7 px-1.5 rounded-sm justify-center items-center gap-1.5 inline-flex text-sm font-medium text-secondary-11 cursor-pointer z-[1] hover:text-secondary-12 transition-color text-nowrap data-[selected]:text-secondary-12 data-[disabled]:cursor-not-allowed data-[disabled]:text-secondary-8" |
| 21 | + INDICATOR = "absolute top-1/2 left-0 -z-1 h-7 w-(--active-tab-width) -translate-y-1/2 translate-x-(--active-tab-left) rounded-sm bg-secondary-1 shadow-small transition-all duration-200 ease-in-out" |
| 22 | + PANEL = "flex flex-col gap-2 p-2" |
| 23 | + |
| 24 | + |
| 25 | +class TabsBaseComponent(BaseUIComponent): |
| 26 | + """Base component for tabs components.""" |
| 27 | + |
| 28 | + library = f"{PACKAGE_NAME}/tabs" |
| 29 | + |
| 30 | + @property |
| 31 | + def import_var(self): |
| 32 | + """Return the import variable for the tabs component.""" |
| 33 | + return ImportVar(tag="Tabs", package_path="", install=False) |
| 34 | + |
| 35 | + |
| 36 | +class TabsRoot(TabsBaseComponent): |
| 37 | + """Groups the tabs and the corresponding panels. Renders a <div> element.""" |
| 38 | + |
| 39 | + tag = "Tabs.Root" |
| 40 | + |
| 41 | + # The default value. Use when the component is not controlled. When the value is null, no Tab will be selected. Defaults to 0. |
| 42 | + default_value: Var[str | int] |
| 43 | + |
| 44 | + # The value of the currently selected Tab. Use when the component is controlled. When the value is null, no Tab will be selected. |
| 45 | + value: Var[str | int] |
| 46 | + |
| 47 | + # Callback invoked when new value is being set. |
| 48 | + on_value_change: EventHandler[passthrough_event_spec(str | dict)] |
| 49 | + |
| 50 | + # The component orientation (layout flow direction). Defaults to "horizontal". |
| 51 | + orientation: Var[LiteralOrientation] |
| 52 | + |
| 53 | + # The render prop |
| 54 | + render_: Var[Component] |
| 55 | + |
| 56 | + @classmethod |
| 57 | + def create(cls, *children, **props) -> Component: |
| 58 | + """Create the tabs root component.""" |
| 59 | + props["data-slot"] = "tabs" |
| 60 | + cls.set_class_name(ClassNames.ROOT, props) |
| 61 | + return super().create(*children, **props) |
| 62 | + |
| 63 | + |
| 64 | +class TabsList(TabsBaseComponent): |
| 65 | + """Groups the individual tab buttons. Renders a <div> element.""" |
| 66 | + |
| 67 | + tag = "Tabs.List" |
| 68 | + |
| 69 | + # Whether to automatically change the active tab on arrow key focus. Otherwise, tabs will be activated using Enter or Spacebar key press. Defaults to True. |
| 70 | + activate_on_focus: Var[bool] |
| 71 | + |
| 72 | + # Whether to loop keyboard focus back to the first item when the end of the list is reached while using the arrow keys. Defaults to True. |
| 73 | + loop: Var[bool] |
| 74 | + |
| 75 | + @classmethod |
| 76 | + def create(cls, *children, **props) -> Component: |
| 77 | + """Create the tabs list component.""" |
| 78 | + props["data-slot"] = "tabs-list" |
| 79 | + cls.set_class_name(ClassNames.LIST, props) |
| 80 | + return super().create(*children, **props) |
| 81 | + |
| 82 | + |
| 83 | +class TabsTab(TabsBaseComponent): |
| 84 | + """An individual interactive tab button that toggles the corresponding panel. Renders a <button> element.""" |
| 85 | + |
| 86 | + tag = "Tabs.Tab" |
| 87 | + |
| 88 | + # The value of the Tab. When not specified, the value is the child position index. |
| 89 | + value: Var[str | int] |
| 90 | + |
| 91 | + # Whether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (e.g. <div>). Defaults to True. |
| 92 | + native_button: Var[bool] |
| 93 | + |
| 94 | + # Whether the Tab is disabled. Defaults to false. |
| 95 | + disabled: Var[bool] |
| 96 | + |
| 97 | + @classmethod |
| 98 | + def create(cls, *children, **props) -> Component: |
| 99 | + """Create the tabs tab component.""" |
| 100 | + props["data-slot"] = "tabs-tab" |
| 101 | + cls.set_class_name(ClassNames.TAB, props) |
| 102 | + return super().create(*children, **props) |
| 103 | + |
| 104 | + |
| 105 | +class TabsIndicator(TabsBaseComponent): |
| 106 | + """A visual indicator that can be styled to match the position of the currently active tab. Renders a <span> element.""" |
| 107 | + |
| 108 | + tag = "Tabs.Indicator" |
| 109 | + |
| 110 | + # Whether to render itself before React hydrates. This minimizes the time that the indicator isn't visible after server-side rendering. Defaults to False. |
| 111 | + render_before_hydration: Var[bool] |
| 112 | + |
| 113 | + @classmethod |
| 114 | + def create(cls, *children, **props) -> Component: |
| 115 | + """Create the tabs indicator component.""" |
| 116 | + props["data-slot"] = "tabs-indicator" |
| 117 | + cls.set_class_name(ClassNames.INDICATOR, props) |
| 118 | + return super().create(*children, **props) |
| 119 | + |
| 120 | + |
| 121 | +class TabsPanel(TabsBaseComponent): |
| 122 | + """A panel displayed when the corresponding tab is active. Renders a <div> element.""" |
| 123 | + |
| 124 | + tag = "Tabs.Panel" |
| 125 | + |
| 126 | + # The value of the TabPanel. It will be shown when the Tab with the corresponding value is selected. If not provided, it will fall back to the index of the panel. It is recommended to explicitly provide it, as it's required for the tab panel to be rendered on the server. |
| 127 | + value: Var[str | int] |
| 128 | + |
| 129 | + # Whether to keep the HTML element in the DOM while the panel is hidden. Defaults to False. |
| 130 | + keep_mounted: Var[bool] |
| 131 | + |
| 132 | + @classmethod |
| 133 | + def create(cls, *children, **props) -> Component: |
| 134 | + """Create the tabs panel component.""" |
| 135 | + props["data-slot"] = "tabs-panel" |
| 136 | + cls.set_class_name(ClassNames.PANEL, props) |
| 137 | + return super().create(*children, **props) |
| 138 | + |
| 139 | + |
| 140 | +class Tabs(ComponentNamespace): |
| 141 | + """Namespace for Tabs components.""" |
| 142 | + |
| 143 | + root = __call__ = staticmethod(TabsRoot.create) |
| 144 | + list = staticmethod(TabsList.create) |
| 145 | + tab = staticmethod(TabsTab.create) |
| 146 | + panel = staticmethod(TabsPanel.create) |
| 147 | + indicator = staticmethod(TabsIndicator.create) |
| 148 | + |
| 149 | + |
| 150 | +tabs = Tabs() |
0 commit comments