558 lines
17 KiB
Markdown
558 lines
17 KiB
Markdown
# Dropdown Component
|
|
|
|
## Introduction
|
|
|
|
The Dropdown component provides an interactive dropdown menu that toggles open or closed when clicking a trigger button. It handles positioning, automatic closing behavior, and keyboard navigation out of the box.
|
|
|
|
**Key features:**
|
|
|
|
- Toggle open/close on button click
|
|
- Automatic close when clicking outside
|
|
- Keyboard support (ESC to close)
|
|
- Configurable vertical position (above or below the button)
|
|
- Configurable horizontal alignment (left, right, or center)
|
|
- Session-based state management
|
|
- HTMX-powered updates without page reload
|
|
|
|
**Common use cases:**
|
|
|
|
- Navigation menus
|
|
- User account menus
|
|
- Action menus (edit, delete, share)
|
|
- Filter or sort options
|
|
- Context-sensitive toolbars
|
|
- Settings quick access
|
|
|
|
## Quick Start
|
|
|
|
Here's a minimal example showing a dropdown menu with navigation links:
|
|
|
|
```python
|
|
from fasthtml.common import *
|
|
from myfasthtml.controls.Dropdown import Dropdown
|
|
from myfasthtml.core.instances import RootInstance
|
|
|
|
# Create root instance and dropdown
|
|
root = RootInstance(session)
|
|
|
|
dropdown = Dropdown(
|
|
parent=root,
|
|
button=Button("Menu", cls="btn"),
|
|
content=Ul(
|
|
Li(A("Home", href="/")),
|
|
Li(A("Settings", href="/settings")),
|
|
Li(A("Logout", href="/logout"))
|
|
)
|
|
)
|
|
|
|
# Render the dropdown
|
|
return dropdown
|
|
```
|
|
|
|
This creates a complete dropdown with:
|
|
|
|
- A "Menu" button that toggles the dropdown
|
|
- A list of navigation links displayed below the button
|
|
- Automatic closing when clicking outside the dropdown
|
|
- ESC key support to close the dropdown
|
|
|
|
**Note:** The dropdown opens below the button and aligns to the left by default. Users can click anywhere outside the dropdown to close it, or press ESC on the keyboard.
|
|
|
|
## Basic Usage
|
|
|
|
### Visual Structure
|
|
|
|
The Dropdown component consists of a trigger button and a content panel:
|
|
|
|
```
|
|
Closed state:
|
|
┌──────────────┐
|
|
│ Button ▼ │
|
|
└──────────────┘
|
|
|
|
Open state (position="below", align="left"):
|
|
┌──────────────┐
|
|
│ Button ▼ │
|
|
├──────────────┴─────────┐
|
|
│ Dropdown Content │
|
|
│ - Option 1 │
|
|
│ - Option 2 │
|
|
│ - Option 3 │
|
|
└────────────────────────┘
|
|
|
|
Open state (position="above", align="right"):
|
|
┌────────────────────────┐
|
|
│ Dropdown Content │
|
|
│ - Option 1 │
|
|
│ - Option 2 │
|
|
├──────────────┬─────────┘
|
|
│ Button ▲ │
|
|
└──────────────┘
|
|
```
|
|
|
|
**Component details:**
|
|
|
|
| Element | Description |
|
|
|-----------|------------------------------------------------|
|
|
| Button | Trigger element that toggles the dropdown |
|
|
| Content | Panel containing the dropdown menu items |
|
|
| Wrapper | Container with relative positioning for anchor |
|
|
|
|
### Creating a Dropdown
|
|
|
|
The Dropdown is a `MultipleInstance`, meaning you can create multiple independent dropdowns in your application. Create it by providing a parent instance:
|
|
|
|
```python
|
|
dropdown = Dropdown(parent=root_instance, button=my_button, content=my_content)
|
|
|
|
# Or with a custom ID
|
|
dropdown = Dropdown(parent=root_instance, button=my_button, content=my_content, _id="my-dropdown")
|
|
```
|
|
|
|
### Button and Content
|
|
|
|
The dropdown requires two main elements:
|
|
|
|
**Button:** The trigger element that users click to toggle the dropdown.
|
|
|
|
```python
|
|
# Simple text button
|
|
dropdown = Dropdown(
|
|
parent=root,
|
|
button=Button("Click me", cls="btn btn-primary"),
|
|
content=my_content
|
|
)
|
|
|
|
# Button with icon
|
|
dropdown = Dropdown(
|
|
parent=root,
|
|
button=Div(
|
|
icon_svg,
|
|
Span("Options"),
|
|
cls="flex items-center gap-2"
|
|
),
|
|
content=my_content
|
|
)
|
|
|
|
# Just an icon
|
|
dropdown = Dropdown(
|
|
parent=root,
|
|
button=icon_svg,
|
|
content=my_content
|
|
)
|
|
```
|
|
|
|
**Content:** Any FastHTML element to display in the dropdown panel.
|
|
|
|
```python
|
|
# Simple list
|
|
content = Ul(
|
|
Li("Option 1"),
|
|
Li("Option 2"),
|
|
Li("Option 3"),
|
|
cls="menu"
|
|
)
|
|
|
|
# Complex content with sections
|
|
content = Div(
|
|
Div("User Actions", cls="font-bold p-2"),
|
|
Hr(),
|
|
Button("Edit Profile", cls="btn btn-ghost w-full"),
|
|
Button("Settings", cls="btn btn-ghost w-full"),
|
|
Hr(),
|
|
Button("Logout", cls="btn btn-error w-full")
|
|
)
|
|
```
|
|
|
|
### Positioning Options
|
|
|
|
The Dropdown supports two positioning parameters:
|
|
|
|
**`position`** - Vertical position relative to the button:
|
|
- `"below"` (default): Dropdown appears below the button
|
|
- `"above"`: Dropdown appears above the button
|
|
|
|
**`align`** - Horizontal alignment relative to the button:
|
|
- `"left"` (default): Dropdown aligns to the left edge of the button
|
|
- `"right"`: Dropdown aligns to the right edge of the button
|
|
- `"center"`: Dropdown is centered relative to the button
|
|
|
|
```python
|
|
# Default: below + left
|
|
dropdown = Dropdown(parent=root, button=btn, content=menu)
|
|
|
|
# Above the button, aligned right
|
|
dropdown = Dropdown(parent=root, button=btn, content=menu, position="above", align="right")
|
|
|
|
# Below the button, centered
|
|
dropdown = Dropdown(parent=root, button=btn, content=menu, position="below", align="center")
|
|
```
|
|
|
|
**Visual examples of all combinations:**
|
|
|
|
```
|
|
position="below", align="left" position="below", align="center" position="below", align="right"
|
|
┌────────┐ ┌────────┐ ┌────────┐
|
|
│ Button │ │ Button │ │ Button │
|
|
├────────┴────┐ ┌────┴────────┴────┐ ┌────────────┴────────┤
|
|
│ Content │ │ Content │ │ Content │
|
|
└─────────────┘ └──────────────────┘ └─────────────────────┘
|
|
|
|
position="above", align="left" position="above", align="center" position="above", align="right"
|
|
┌─────────────┐ ┌──────────────────┐ ┌─────────────────────┐
|
|
│ Content │ │ Content │ │ Content │
|
|
├────────┬────┘ └────┬────────┬────┘ └────────────┬────────┤
|
|
│ Button │ │ Button │ │ Button │
|
|
└────────┘ └────────┘ └────────┘
|
|
```
|
|
|
|
## Advanced Features
|
|
|
|
### Automatic Close Behavior
|
|
|
|
The Dropdown automatically closes in two scenarios:
|
|
|
|
**Click outside:** When the user clicks anywhere outside the dropdown, it closes automatically. This is handled by the Mouse component listening for global click events.
|
|
|
|
**ESC key:** When the user presses the ESC key, the dropdown closes. This is handled by the Keyboard component.
|
|
|
|
```python
|
|
# Both behaviors are enabled by default - no configuration needed
|
|
dropdown = Dropdown(parent=root, button=btn, content=menu)
|
|
```
|
|
|
|
**How it works internally:**
|
|
|
|
- The `Mouse` component detects clicks and sends `is_inside` and `is_button` parameters
|
|
- If `is_button` is true, the dropdown toggles
|
|
- If `is_inside` is false (clicked outside), the dropdown closes
|
|
- The `Keyboard` component listens for ESC and triggers the close command
|
|
|
|
### Programmatic Control
|
|
|
|
You can control the dropdown programmatically using its methods and commands:
|
|
|
|
```python
|
|
# Toggle the dropdown state
|
|
dropdown.toggle()
|
|
|
|
# Close the dropdown
|
|
dropdown.close()
|
|
|
|
# Access commands for use with other controls
|
|
close_cmd = dropdown.commands.close()
|
|
click_cmd = dropdown.commands.click()
|
|
```
|
|
|
|
**Using commands with buttons:**
|
|
|
|
```python
|
|
from myfasthtml.controls.helpers import mk
|
|
|
|
# Create a button that closes the dropdown
|
|
close_button = mk.button("Close", command=dropdown.commands.close())
|
|
|
|
# Add it to the dropdown content
|
|
dropdown = Dropdown(
|
|
parent=root,
|
|
button=Button("Menu"),
|
|
content=Div(
|
|
Ul(Li("Option 1"), Li("Option 2")),
|
|
close_button
|
|
)
|
|
)
|
|
```
|
|
|
|
### CSS Customization
|
|
|
|
The Dropdown uses CSS classes that you can customize:
|
|
|
|
| Class | Element |
|
|
|-----------------------|---------------------------------------|
|
|
| `mf-dropdown-wrapper` | Container with relative positioning |
|
|
| `mf-dropdown-btn` | Button wrapper |
|
|
| `mf-dropdown` | Dropdown content panel |
|
|
| `mf-dropdown-below` | Applied when position="below" |
|
|
| `mf-dropdown-above` | Applied when position="above" |
|
|
| `mf-dropdown-left` | Applied when align="left" |
|
|
| `mf-dropdown-right` | Applied when align="right" |
|
|
| `mf-dropdown-center` | Applied when align="center" |
|
|
| `is-visible` | Applied when dropdown is open |
|
|
|
|
**Example customization:**
|
|
|
|
```css
|
|
/* Change dropdown background and border */
|
|
.mf-dropdown {
|
|
background-color: #1f2937;
|
|
border: 1px solid #374151;
|
|
border-radius: 0.5rem;
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
/* Add animation */
|
|
.mf-dropdown {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
}
|
|
|
|
.mf-dropdown.is-visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
/* Style for above position */
|
|
.mf-dropdown-above {
|
|
transform: translateY(10px);
|
|
}
|
|
|
|
.mf-dropdown-above.is-visible {
|
|
transform: translateY(0);
|
|
}
|
|
```
|
|
|
|
## Examples
|
|
|
|
### Example 1: Navigation Menu
|
|
|
|
A simple navigation dropdown menu:
|
|
|
|
```python
|
|
from fasthtml.common import *
|
|
from myfasthtml.controls.Dropdown import Dropdown
|
|
|
|
dropdown = Dropdown(
|
|
parent=root,
|
|
button=Button("Navigation", cls="btn btn-ghost"),
|
|
content=Ul(
|
|
Li(A("Dashboard", href="/dashboard", cls="block p-2 hover:bg-base-200")),
|
|
Li(A("Projects", href="/projects", cls="block p-2 hover:bg-base-200")),
|
|
Li(A("Tasks", href="/tasks", cls="block p-2 hover:bg-base-200")),
|
|
Li(A("Reports", href="/reports", cls="block p-2 hover:bg-base-200")),
|
|
cls="menu p-2"
|
|
)
|
|
)
|
|
|
|
return dropdown
|
|
```
|
|
|
|
### Example 2: User Account Menu
|
|
|
|
A user menu aligned to the right, typically placed in a header:
|
|
|
|
```python
|
|
from fasthtml.common import *
|
|
from myfasthtml.controls.Dropdown import Dropdown
|
|
|
|
# User avatar button
|
|
user_button = Div(
|
|
Img(src="/avatar.png", cls="w-8 h-8 rounded-full"),
|
|
Span("John Doe", cls="ml-2"),
|
|
cls="flex items-center gap-2 cursor-pointer"
|
|
)
|
|
|
|
# Account menu content
|
|
account_menu = Div(
|
|
Div(
|
|
Div("John Doe", cls="font-bold"),
|
|
Div("john@example.com", cls="text-sm opacity-60"),
|
|
cls="p-3 border-b"
|
|
),
|
|
Ul(
|
|
Li(A("Profile", href="/profile", cls="block p-2 hover:bg-base-200")),
|
|
Li(A("Settings", href="/settings", cls="block p-2 hover:bg-base-200")),
|
|
Li(A("Billing", href="/billing", cls="block p-2 hover:bg-base-200")),
|
|
cls="menu p-2"
|
|
),
|
|
Div(
|
|
A("Sign out", href="/logout", cls="block p-2 text-error hover:bg-base-200"),
|
|
cls="border-t"
|
|
),
|
|
cls="w-56"
|
|
)
|
|
|
|
# Align right so it doesn't overflow the viewport
|
|
dropdown = Dropdown(
|
|
parent=root,
|
|
button=user_button,
|
|
content=account_menu,
|
|
align="right"
|
|
)
|
|
|
|
return dropdown
|
|
```
|
|
|
|
### Example 3: Action Menu Above Button
|
|
|
|
A dropdown that opens above the trigger, useful when the button is at the bottom of the screen:
|
|
|
|
```python
|
|
from fasthtml.common import *
|
|
from myfasthtml.controls.Dropdown import Dropdown
|
|
|
|
# Action button with icon
|
|
action_button = Button(
|
|
Span("+", cls="text-xl"),
|
|
cls="btn btn-circle btn-primary"
|
|
)
|
|
|
|
# Quick actions menu
|
|
actions_menu = Div(
|
|
Button("New Document", cls="btn btn-ghost btn-sm w-full justify-start"),
|
|
Button("Upload File", cls="btn btn-ghost btn-sm w-full justify-start"),
|
|
Button("Create Folder", cls="btn btn-ghost btn-sm w-full justify-start"),
|
|
Button("Import Data", cls="btn btn-ghost btn-sm w-full justify-start"),
|
|
cls="flex flex-col p-2 w-40"
|
|
)
|
|
|
|
# Open above and center-aligned
|
|
dropdown = Dropdown(
|
|
parent=root,
|
|
button=action_button,
|
|
content=actions_menu,
|
|
position="above",
|
|
align="center"
|
|
)
|
|
|
|
return dropdown
|
|
```
|
|
|
|
### Example 4: Dropdown with Commands
|
|
|
|
A dropdown containing action buttons that execute commands:
|
|
|
|
```python
|
|
from fasthtml.common import *
|
|
from myfasthtml.controls.Dropdown import Dropdown
|
|
from myfasthtml.controls.helpers import mk
|
|
from myfasthtml.core.commands import Command
|
|
|
|
# Define actions
|
|
def edit_item():
|
|
return "Editing..."
|
|
|
|
def delete_item():
|
|
return "Deleted!"
|
|
|
|
def share_item():
|
|
return "Shared!"
|
|
|
|
# Create commands
|
|
edit_cmd = Command("edit", "Edit item", edit_item)
|
|
delete_cmd = Command("delete", "Delete item", delete_item)
|
|
share_cmd = Command("share", "Share item", share_item)
|
|
|
|
# Build menu with command buttons
|
|
actions_menu = Div(
|
|
mk.button("Edit", command=edit_cmd, cls="btn btn-ghost btn-sm w-full justify-start"),
|
|
mk.button("Share", command=share_cmd, cls="btn btn-ghost btn-sm w-full justify-start"),
|
|
Hr(cls="my-1"),
|
|
mk.button("Delete", command=delete_cmd, cls="btn btn-ghost btn-sm w-full justify-start text-error"),
|
|
cls="flex flex-col p-2"
|
|
)
|
|
|
|
dropdown = Dropdown(
|
|
parent=root,
|
|
button=Button("Actions", cls="btn btn-sm"),
|
|
content=actions_menu
|
|
)
|
|
|
|
return dropdown
|
|
```
|
|
|
|
---
|
|
|
|
## Developer Reference
|
|
|
|
This section contains technical details for developers working on the Dropdown component itself.
|
|
|
|
### State
|
|
|
|
The Dropdown component maintains its state via `DropdownState`:
|
|
|
|
| Name | Type | Description | Default |
|
|
|----------|---------|------------------------------|---------|
|
|
| `opened` | boolean | Whether dropdown is open | `False` |
|
|
|
|
### Commands
|
|
|
|
Available commands for programmatic control:
|
|
|
|
| Name | Description |
|
|
|-----------|-------------------------------------------------|
|
|
| `close()` | Closes the dropdown |
|
|
| `click()` | Handles click events (toggle or close behavior) |
|
|
|
|
**Command details:**
|
|
|
|
- `close()`: Sets `opened` to `False` and returns updated content
|
|
- `click()`: Receives `combination`, `is_inside`, and `is_button` parameters
|
|
- If `is_button` is `True`: toggles the dropdown
|
|
- If `is_inside` is `False`: closes the dropdown
|
|
|
|
### Public Methods
|
|
|
|
| Method | Description | Returns |
|
|
|------------|----------------------------|----------------------|
|
|
| `toggle()` | Toggles open/closed state | Content tuple |
|
|
| `close()` | Closes the dropdown | Content tuple |
|
|
| `render()` | Renders complete component | `Div` |
|
|
|
|
### Constructor Parameters
|
|
|
|
| Parameter | Type | Description | Default |
|
|
|------------|-------------|------------------------------------|-----------|
|
|
| `parent` | Instance | Parent instance (required) | - |
|
|
| `content` | Any | Content to display in dropdown | `None` |
|
|
| `button` | Any | Trigger element | `None` |
|
|
| `_id` | str | Custom ID for the instance | `None` |
|
|
| `position` | str | Vertical position: "below"/"above" | `"below"` |
|
|
| `align` | str | Horizontal align: "left"/"right"/"center" | `"left"` |
|
|
|
|
### High Level Hierarchical Structure
|
|
|
|
```
|
|
Div(id="{id}")
|
|
├── Div(cls="mf-dropdown-wrapper")
|
|
│ ├── Div(cls="mf-dropdown-btn")
|
|
│ │ └── [Button content]
|
|
│ └── Div(id="{id}-content", cls="mf-dropdown mf-dropdown-{position} mf-dropdown-{align} [is-visible]")
|
|
│ └── [Dropdown content]
|
|
├── Keyboard(id="{id}-keyboard")
|
|
│ └── ESC → close command
|
|
└── Mouse(id="{id}-mouse")
|
|
└── click → click command
|
|
```
|
|
|
|
### Element IDs
|
|
|
|
| Name | Description |
|
|
|------------------|--------------------------------|
|
|
| `{id}` | Root dropdown container |
|
|
| `{id}-content` | Dropdown content panel |
|
|
| `{id}-keyboard` | Keyboard handler component |
|
|
| `{id}-mouse` | Mouse handler component |
|
|
|
|
**Note:** `{id}` is the Dropdown instance ID (auto-generated or custom `_id`).
|
|
|
|
### Internal Methods
|
|
|
|
| Method | Description |
|
|
|-----------------|------------------------------------------|
|
|
| `_mk_content()` | Renders the dropdown content panel |
|
|
| `on_click()` | Handles click events from Mouse component |
|
|
|
|
**Method details:**
|
|
|
|
- `_mk_content()`:
|
|
- Builds CSS classes based on `position` and `align`
|
|
- Adds `is-visible` class when `opened` is `True`
|
|
- Returns a tuple containing the content `Div`
|
|
|
|
- `on_click(combination, is_inside, is_button)`:
|
|
- Called by Mouse component on click events
|
|
- `is_button`: `True` if click was on the button
|
|
- `is_inside`: `True` if click was inside the dropdown
|
|
- Returns updated content for HTMX swap
|