Working on ColumnsManager. Added CycleStateControl and DataGridColumnsManager.

This commit is contained in:
2026-01-24 23:55:44 +01:00
parent 3c2c07ebfc
commit 0bd56c7f09
9 changed files with 1080 additions and 103 deletions

557
docs/Dropdown.md Normal file
View File

@@ -0,0 +1,557 @@
# 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