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()