from fastcore.xml import FT from fasthtml.components import Div from myfasthtml.controls.BaseCommands import BaseCommands from myfasthtml.controls.Keyboard import Keyboard from myfasthtml.controls.Mouse import Mouse from myfasthtml.core.commands import Command from myfasthtml.core.instances import MultipleInstance class Commands(BaseCommands): def close(self): return Command("Close", "Close Dropdown", self._owner, self._owner.close).htmx(target=f"#{self._owner.get_id()}-content") def click(self): return Command("Click", "Click on Dropdown", self._owner, self._owner.on_click).htmx(target=f"#{self._owner.get_id()}-content") class DropdownState: def __init__(self): self.opened = False class Dropdown(MultipleInstance): """ Interactive dropdown component that toggles open/closed on button click. Provides automatic close behavior when clicking outside or pressing ESC. Supports configurable positioning relative to the trigger button. Args: parent: Parent instance (required). content: Content to display in the dropdown panel. button: Trigger element that toggles the dropdown. _id: Custom ID for the instance. position: Vertical position relative to button. - "below" (default): Dropdown appears below the button. - "above": Dropdown appears above the button. align: Horizontal alignment relative to button. - "left" (default): Aligns to the left edge of the button. - "right": Aligns to the right edge of the button. - "center": Centers relative to the button. Example: dropdown = Dropdown( parent=root, button=Button("Menu"), content=Ul(Li("Option 1"), Li("Option 2")), position="below", align="right" ) """ def __init__(self, parent, content=None, button=None, _id=None, position="below", align="left"): super().__init__(parent, _id=_id) self.button = Div(button) if not isinstance(button, FT) else button self.content = content self.commands = Commands(self) self._state = DropdownState() self._position = position self._align = align def toggle(self): self._state.opened = not self._state.opened return self._mk_content() def close(self): self._state.opened = False return self._mk_content() def on_click(self, combination, is_inside: bool, is_button: bool = False): if combination == "click": if is_button: self._state.opened = not self._state.opened else: self._state.opened = is_inside return self._mk_content() def _mk_content(self): position_cls = f"mf-dropdown-{self._position}" align_cls = f"mf-dropdown-{self._align}" return Div(self.content, cls=f"mf-dropdown {position_cls} {align_cls} {'is-visible' if self._state.opened else ''}", id=f"{self._id}-content"), def render(self): return Div( Div( Div(self.button if self.button else "None", cls="mf-dropdown-btn"), self._mk_content(), cls="mf-dropdown-wrapper" ), Keyboard(self, _id="-keyboard").add("esc", self.commands.close()), Mouse(self, "-mouse").add("click", self.commands.click(), hx_vals="js:getDropdownExtra()"), id=self._id ) def __ft__(self): return self.render()