Added IconsHelper and updated Keyboard to support require_inside flag
This commit is contained in:
@@ -21,19 +21,47 @@
|
||||
|
||||
## Key Features
|
||||
|
||||
### Multiple Simultaneous Triggers
|
||||
### Scope Control with `require_inside`
|
||||
|
||||
**IMPORTANT**: If multiple elements listen to the same combination, **ALL** of them will be triggered:
|
||||
Each combination can declare whether it should only trigger when the focus is **inside** the registered element, or fire **globally** regardless of focus.
|
||||
|
||||
| `require_inside` | Behavior |
|
||||
|-----------------|----------|
|
||||
| `true` (default) | Triggers only if focus is inside the element or one of its children |
|
||||
| `false` | Triggers regardless of where the focus is (global shortcut) |
|
||||
|
||||
```javascript
|
||||
add_keyboard_support('modal', '{"esc": "/close-modal"}');
|
||||
add_keyboard_support('editor', '{"esc": "/cancel-edit"}');
|
||||
add_keyboard_support('sidebar', '{"esc": "/hide-sidebar"}');
|
||||
// Only fires when focus is inside #tree-panel
|
||||
add_keyboard_support('tree-panel', '{"esc": {"hx-post": "/cancel", "require_inside": true}}');
|
||||
|
||||
// Pressing ESC will trigger all 3 URLs simultaneously
|
||||
// Fires anywhere on the page
|
||||
add_keyboard_support('app', '{"ctrl+n": {"hx-post": "/new", "require_inside": false}}');
|
||||
```
|
||||
|
||||
This is crucial for use cases like the ESC key, which often needs to cancel multiple actions at once (close modal, cancel edit, hide panels, etc.).
|
||||
**Python usage (`Keyboard` component):**
|
||||
|
||||
```python
|
||||
# Default: require_inside=True — fires only when inside the element
|
||||
Keyboard(self, _id="-kb").add("esc", self.commands.cancel())
|
||||
|
||||
# Explicit global shortcut
|
||||
Keyboard(self, _id="-kb").add("ctrl+n", self.commands.new_item(), require_inside=False)
|
||||
```
|
||||
|
||||
### Multiple Simultaneous Triggers
|
||||
|
||||
**IMPORTANT**: If multiple elements listen to the same combination, all of them whose `require_inside` condition is satisfied will be triggered simultaneously:
|
||||
|
||||
```javascript
|
||||
add_keyboard_support('modal', '{"esc": {"hx-post": "/close-modal", "require_inside": true}}');
|
||||
add_keyboard_support('editor', '{"esc": {"hx-post": "/cancel-edit", "require_inside": true}}');
|
||||
add_keyboard_support('sidebar', '{"esc": {"hx-post": "/hide-sidebar", "require_inside": false}}');
|
||||
|
||||
// Pressing ESC while focus is inside 'editor':
|
||||
// - 'modal' → skipped (require_inside: true, focus not inside)
|
||||
// - 'editor' → triggered ✓
|
||||
// - 'sidebar' → triggered ✓ (require_inside: false)
|
||||
```
|
||||
|
||||
### Smart Timeout Logic (Longest Match)
|
||||
|
||||
@@ -232,6 +260,8 @@ The library automatically adds these parameters to every request:
|
||||
- `has_focus` - Boolean indicating if the element had focus
|
||||
- `is_inside` - Boolean indicating if the focus is inside the element (element itself or any child)
|
||||
|
||||
Note: `require_inside` controls **whether** the action fires; `is_inside` is an informational parameter sent **with** the request after it fires.
|
||||
|
||||
Example final request:
|
||||
```javascript
|
||||
htmx.ajax('POST', '/save-url', {
|
||||
|
||||
@@ -165,7 +165,6 @@
|
||||
|
||||
// Add key to current pressed keys
|
||||
KeyboardRegistry.currentKeys.add(key);
|
||||
// console.debug("Received key", key);
|
||||
|
||||
// Create a snapshot of current keyboard state
|
||||
const snapshot = new Set(KeyboardRegistry.currentKeys);
|
||||
@@ -218,14 +217,17 @@
|
||||
anyHasLongerSequence = true;
|
||||
}
|
||||
|
||||
// Collect matches
|
||||
// Collect matches, respecting require_inside flag
|
||||
if (hasMatch) {
|
||||
currentMatches.push({
|
||||
elementId: elementId,
|
||||
config: currentNode.config,
|
||||
combinationStr: currentNode.combinationStr,
|
||||
isInside: isInside
|
||||
});
|
||||
const requireInside = currentNode.config["require_inside"] === true;
|
||||
if (!requireInside || isInside) {
|
||||
currentMatches.push({
|
||||
elementId: elementId,
|
||||
config: currentNode.config,
|
||||
combinationStr: currentNode.combinationStr,
|
||||
isInside: isInside
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,13 @@ from myfasthtml.controls.DataGridColumnsManager import DataGridColumnsManager
|
||||
from myfasthtml.controls.DataGridFormattingEditor import DataGridFormattingEditor
|
||||
from myfasthtml.controls.DataGridQuery import DataGridQuery, DG_QUERY_FILTER
|
||||
from myfasthtml.controls.DslEditor import DslEditorConf
|
||||
from myfasthtml.controls.IconsHelper import IconsHelper
|
||||
from myfasthtml.controls.Keyboard import Keyboard
|
||||
from myfasthtml.controls.Mouse import Mouse
|
||||
from myfasthtml.controls.Panel import Panel, PanelConf
|
||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
|
||||
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
|
||||
from myfasthtml.controls.helpers import mk, icons, column_type_defaults
|
||||
from myfasthtml.controls.helpers import mk, column_type_defaults
|
||||
from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
|
||||
from myfasthtml.core.dbmanager import DbObject
|
||||
@@ -261,7 +262,7 @@ class DataGrid(MultipleInstance):
|
||||
}
|
||||
|
||||
self._key_support = {
|
||||
"esc": self.commands.on_key_pressed(),
|
||||
"esc": {"command": self.commands.on_key_pressed(), "require_inside": True},
|
||||
}
|
||||
|
||||
logger.debug(f"DataGrid '{self.get_table_name()}' with id='{self._id}' created.")
|
||||
@@ -647,7 +648,7 @@ class DataGrid(MultipleInstance):
|
||||
|
||||
def _mk_header_name(col_def: DataGridColumnState):
|
||||
return Div(
|
||||
mk.label(col_def.title, icon=icons.get(col_def.type, None)),
|
||||
mk.label(col_def.title, icon=IconsHelper.get(col_def.type)),
|
||||
# make room for sort and filter indicators
|
||||
cls="flex truncate cursor-default",
|
||||
data_tooltip=col_def.title,
|
||||
|
||||
@@ -5,9 +5,10 @@ from fasthtml.components import *
|
||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||
from myfasthtml.controls.DataGridFormulaEditor import DataGridFormulaEditor
|
||||
from myfasthtml.controls.DslEditor import DslEditorConf
|
||||
from myfasthtml.controls.IconsHelper import IconsHelper
|
||||
from myfasthtml.controls.Search import Search
|
||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState
|
||||
from myfasthtml.controls.helpers import icons, mk
|
||||
from myfasthtml.controls.helpers import mk
|
||||
from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.constants import ColumnType
|
||||
from myfasthtml.core.dsls import DslsManager
|
||||
@@ -179,7 +180,7 @@ class DataGridColumnsManager(MultipleInstance):
|
||||
),
|
||||
mk.mk(
|
||||
Div(
|
||||
Div(mk.label(col_def.col_id, icon=icons.get(col_def.type, None), cls="ml-2")),
|
||||
Div(mk.label(col_def.col_id, icon=IconsHelper.get(col_def.type), cls="ml-2")),
|
||||
Div(mk.icon(chevron_right20_regular), cls="mr-2"),
|
||||
cls="dt2-column-manager-label"
|
||||
),
|
||||
|
||||
@@ -97,7 +97,7 @@ class Dropdown(MultipleInstance):
|
||||
self._mk_content(),
|
||||
cls="mf-dropdown-wrapper"
|
||||
),
|
||||
Keyboard(self, _id="-keyboard").add("esc", self.commands.close()),
|
||||
Keyboard(self, _id="-keyboard").add("esc", self.commands.close(), require_inside=True),
|
||||
Mouse(self, "-mouse").add("click", self.commands.click(), hx_vals="js:getDropdownExtra()"),
|
||||
id=self._id
|
||||
)
|
||||
|
||||
71
src/myfasthtml/controls/IconsHelper.py
Normal file
71
src/myfasthtml/controls/IconsHelper.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from myfasthtml.core.constants import ColumnType
|
||||
from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_symbol20_regular, \
|
||||
number_row20_regular
|
||||
from myfasthtml.icons.fluent_p1 import checkbox_checked20_regular, checkbox_unchecked20_regular, \
|
||||
checkbox_checked20_filled, math_formula16_regular
|
||||
from myfasthtml.icons.fluent_p2 import text_field20_regular, text_bullet_list_square20_regular
|
||||
from myfasthtml.icons.fluent_p3 import calendar_ltr20_regular
|
||||
|
||||
default_icons = {
|
||||
None: question20_regular,
|
||||
True: checkbox_checked20_regular,
|
||||
False: checkbox_unchecked20_regular,
|
||||
|
||||
"Brain": brain_circuit20_regular,
|
||||
|
||||
ColumnType.RowIndex: number_symbol20_regular,
|
||||
ColumnType.Text: text_field20_regular,
|
||||
ColumnType.Number: number_row20_regular,
|
||||
ColumnType.Datetime: calendar_ltr20_regular,
|
||||
ColumnType.Bool: checkbox_checked20_filled,
|
||||
ColumnType.Enum: text_bullet_list_square20_regular,
|
||||
ColumnType.Formula: math_formula16_regular,
|
||||
}
|
||||
|
||||
|
||||
class IconsHelper:
|
||||
_icons = default_icons.copy()
|
||||
|
||||
@staticmethod
|
||||
def get(name, package=None):
|
||||
"""
|
||||
Fetches and returns an icon resource based on the provided name and optional package. If the icon is not already
|
||||
cached, it will attempt to dynamically load the icon from the available modules under the `myfasthtml.icons` package.
|
||||
This method uses an internal caching mechanism to store previously fetched icons for future quick lookups.
|
||||
|
||||
:param name: The name of the requested icon.
|
||||
:param package: The optional sub-package to limit the search for the requested icon. If not provided, the method will
|
||||
iterate through all available modules within the `myfasthtml.icons` package.
|
||||
:return: The requested icon resource if found; otherwise, returns None.
|
||||
:rtype: object or None
|
||||
"""
|
||||
if name in IconsHelper._icons:
|
||||
return IconsHelper._icons[name]
|
||||
|
||||
import importlib
|
||||
import pkgutil
|
||||
import myfasthtml.icons as icons_pkg
|
||||
|
||||
_UTILITY_MODULES = {'manage_icons', 'update_icons'}
|
||||
|
||||
if package:
|
||||
module = importlib.import_module(f"myfasthtml.icons.{package}")
|
||||
icon = getattr(module, name, None)
|
||||
if icon is not None:
|
||||
IconsHelper._icons[name] = icon
|
||||
return icon
|
||||
|
||||
for _, modname, _ in pkgutil.iter_modules(icons_pkg.__path__):
|
||||
if modname in _UTILITY_MODULES:
|
||||
continue
|
||||
module = importlib.import_module(f"myfasthtml.icons.{modname}")
|
||||
icon = getattr(module, name, None)
|
||||
if icon is not None:
|
||||
IconsHelper._icons[name] = icon
|
||||
return icon
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def reset():
|
||||
IconsHelper._icons = default_icons.copy()
|
||||
@@ -17,12 +17,16 @@ class Keyboard(MultipleInstance):
|
||||
super().__init__(parent, _id=_id)
|
||||
self.combinations = combinations or {}
|
||||
|
||||
def add(self, sequence: str, command: Command):
|
||||
self.combinations[sequence] = command
|
||||
def add(self, sequence: str, command: Command, require_inside: bool = True):
|
||||
self.combinations[sequence] = {"command": command, "require_inside": require_inside}
|
||||
return self
|
||||
|
||||
|
||||
def render(self):
|
||||
str_combinations = {sequence: command.get_htmx_params() for sequence, command in self.combinations.items()}
|
||||
str_combinations = {}
|
||||
for sequence, value in self.combinations.items():
|
||||
params = value["command"].get_htmx_params()
|
||||
params["require_inside"] = value.get("require_inside", True)
|
||||
str_combinations[sequence] = params
|
||||
return Script(f"add_keyboard_support('{self._parent.get_id()}', '{json.dumps(str_combinations)}')")
|
||||
|
||||
def __ft__(self):
|
||||
|
||||
@@ -4,6 +4,7 @@ TreeView component for hierarchical data visualization with inline editing.
|
||||
This component provides an interactive tree structure with expand/collapse,
|
||||
selection, and inline editing capabilities.
|
||||
"""
|
||||
import logging
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
@@ -19,6 +20,19 @@ from myfasthtml.core.instances import MultipleInstance
|
||||
from myfasthtml.icons.fluent_p1 import chevron_right20_regular, edit20_regular
|
||||
from myfasthtml.icons.fluent_p2 import chevron_down20_regular, add_circle20_regular, delete20_regular
|
||||
|
||||
logger = logging.getLogger("TreeView")
|
||||
|
||||
|
||||
@dataclass
|
||||
class TreeViewConf:
|
||||
edit_node: bool = True
|
||||
add_node: bool = True
|
||||
delete_node: bool = True
|
||||
edit_leaf: bool = True
|
||||
add_leaf: bool = True
|
||||
delete_leaf: bool = True
|
||||
icons: dict = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TreeNode:
|
||||
@@ -157,7 +171,7 @@ class TreeView(MultipleInstance):
|
||||
- Node selection
|
||||
"""
|
||||
|
||||
def __init__(self, parent, items: Optional[dict] = None, _id: Optional[str] = None):
|
||||
def __init__(self, parent, items: Optional[dict] = None, conf: TreeViewConf = None, _id: Optional[str] = None):
|
||||
"""
|
||||
Initialize TreeView component.
|
||||
|
||||
@@ -168,10 +182,14 @@ class TreeView(MultipleInstance):
|
||||
"""
|
||||
super().__init__(parent, _id=_id)
|
||||
self._state = TreeViewState(self)
|
||||
self.conf = conf or TreeViewConf()
|
||||
self.commands = Commands(self)
|
||||
|
||||
|
||||
if items:
|
||||
self._state.items = items
|
||||
|
||||
if self.conf.icons:
|
||||
self._state.icon_config = self.conf.icons
|
||||
|
||||
def set_icon_config(self, config: dict[str, str]):
|
||||
"""
|
||||
@@ -207,7 +225,7 @@ class TreeView(MultipleInstance):
|
||||
else:
|
||||
parent.children.append(node.id)
|
||||
|
||||
def ensure_path(self, path: str):
|
||||
def ensure_path(self, path: str, node_type="folder"):
|
||||
"""Add a node to the tree based on a path string.
|
||||
|
||||
Args:
|
||||
@@ -237,7 +255,7 @@ class TreeView(MultipleInstance):
|
||||
node = [node for node in current_nodes if node.label == part]
|
||||
if len(node) == 0:
|
||||
# create the node
|
||||
node = TreeNode(label=part, type="folder")
|
||||
node = TreeNode(label=part, type=node_type)
|
||||
self.add_node(node, parent_id=parent_id)
|
||||
else:
|
||||
node = node[0]
|
||||
@@ -341,6 +359,7 @@ class TreeView(MultipleInstance):
|
||||
|
||||
def _cancel_rename(self):
|
||||
"""Cancel renaming operation."""
|
||||
logger.debug("_cancel_rename")
|
||||
self._state.editing = None
|
||||
return self
|
||||
|
||||
@@ -380,12 +399,22 @@ class TreeView(MultipleInstance):
|
||||
|
||||
def _render_action_buttons(self, node_id: str):
|
||||
"""Render action buttons for a node (visible on hover)."""
|
||||
return Div(
|
||||
mk.icon(add_circle20_regular, command=self.commands.add_child(node_id)),
|
||||
mk.icon(edit20_regular, command=self.commands.start_rename(node_id)),
|
||||
mk.icon(delete20_regular, command=self.commands.delete_node(node_id)),
|
||||
cls="mf-treenode-actions"
|
||||
)
|
||||
is_leaf = len(self._state.items[node_id].children) == 0
|
||||
conf = self.conf
|
||||
|
||||
add_visible = conf.add_leaf if is_leaf else conf.add_node
|
||||
edit_visible = conf.edit_leaf if is_leaf else conf.edit_node
|
||||
delete_visible = conf.delete_leaf if is_leaf else conf.delete_node
|
||||
|
||||
buttons = []
|
||||
if add_visible:
|
||||
buttons.append(mk.icon(add_circle20_regular, command=self.commands.add_child(node_id)))
|
||||
if edit_visible:
|
||||
buttons.append(mk.icon(edit20_regular, command=self.commands.start_rename(node_id)))
|
||||
if delete_visible:
|
||||
buttons.append(mk.icon(delete20_regular, command=self.commands.delete_node(node_id)))
|
||||
|
||||
return Div(*buttons, cls="mf-treenode-actions")
|
||||
|
||||
def _render_node(self, node_id: str, level: int = 0):
|
||||
"""
|
||||
@@ -404,10 +433,13 @@ class TreeView(MultipleInstance):
|
||||
is_editing = node_id == self._state.editing
|
||||
has_children = len(node.children) > 0
|
||||
|
||||
# Toggle icon
|
||||
toggle = mk.icon(
|
||||
chevron_down20_regular if is_expanded else chevron_right20_regular if has_children else None,
|
||||
command=self.commands.toggle_node(node_id))
|
||||
# Toggle icon (only for nodes with children)
|
||||
if has_children:
|
||||
toggle = mk.icon(
|
||||
chevron_down20_regular if is_expanded else chevron_right20_regular,
|
||||
command=self.commands.toggle_node(node_id))
|
||||
else:
|
||||
toggle = None
|
||||
|
||||
# Label or input for editing
|
||||
if is_editing:
|
||||
@@ -426,7 +458,7 @@ class TreeView(MultipleInstance):
|
||||
node_element = Div(
|
||||
toggle,
|
||||
label_element,
|
||||
self._render_action_buttons(node_id),
|
||||
*([self._render_action_buttons(node_id)] if not is_editing else []),
|
||||
cls=f"mf-treenode flex {'selected' if is_selected and not is_editing else ''}",
|
||||
style=f"padding-left: {level * 20}px"
|
||||
)
|
||||
@@ -461,7 +493,7 @@ class TreeView(MultipleInstance):
|
||||
|
||||
return Div(
|
||||
*[self._render_node(node_id) for node_id in root_nodes],
|
||||
Keyboard(self, {"esc": self.commands.cancel_rename()}, _id="-keyboard"),
|
||||
Keyboard(self, {"esc": {"command": self.commands.cancel_rename(), "require_inside": False}}, _id="-keyboard"),
|
||||
id=self._id,
|
||||
cls="mf-treeview"
|
||||
)
|
||||
|
||||
@@ -5,12 +5,6 @@ from myfasthtml.core.bindings import Binding
|
||||
from myfasthtml.core.commands import Command, CommandTemplate
|
||||
from myfasthtml.core.constants import ColumnType
|
||||
from myfasthtml.core.utils import merge_classes
|
||||
from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_row20_regular, \
|
||||
number_symbol20_regular
|
||||
from myfasthtml.icons.fluent_p1 import checkbox_checked20_regular, checkbox_unchecked20_regular, \
|
||||
checkbox_checked20_filled, math_formula16_regular
|
||||
from myfasthtml.icons.fluent_p2 import text_bullet_list_square20_regular, text_field20_regular
|
||||
from myfasthtml.icons.fluent_p3 import calendar_ltr20_regular
|
||||
|
||||
|
||||
class Ids:
|
||||
@@ -148,26 +142,9 @@ class mk:
|
||||
return ft
|
||||
|
||||
|
||||
icons = {
|
||||
None: question20_regular,
|
||||
True: checkbox_checked20_regular,
|
||||
False: checkbox_unchecked20_regular,
|
||||
|
||||
"Brain": brain_circuit20_regular,
|
||||
|
||||
ColumnType.RowIndex: number_symbol20_regular,
|
||||
ColumnType.Text: text_field20_regular,
|
||||
ColumnType.Number: number_row20_regular,
|
||||
ColumnType.Datetime: calendar_ltr20_regular,
|
||||
ColumnType.Bool: checkbox_checked20_filled,
|
||||
ColumnType.Enum: text_bullet_list_square20_regular,
|
||||
ColumnType.Formula: math_formula16_regular,
|
||||
}
|
||||
|
||||
column_type_defaults = {
|
||||
ColumnType.Number: 0,
|
||||
ColumnType.Text: "",
|
||||
ColumnType.Bool: False,
|
||||
ColumnType.Datetime: pd.NaT,
|
||||
}
|
||||
|
||||
@@ -88,10 +88,10 @@ class Command:
|
||||
if auto_register:
|
||||
if self._key is not None:
|
||||
if self._key in CommandsManager.commands_by_key:
|
||||
logger.debug(f"Command {self.name} with key={self._key} will not be registered.")
|
||||
#logger.debug(f"Command {self.name} with key={self._key} will not be registered.")
|
||||
self.id = CommandsManager.commands_by_key[self._key].id
|
||||
else:
|
||||
logger.debug(f"Command {self.name} with key={self._key} will be registered.")
|
||||
#logger.debug(f"Command {self.name} with key={self._key} will be registered.")
|
||||
CommandsManager.register(self)
|
||||
else:
|
||||
logger.warning(f"Command {self.name} has no key, it will not be registered.")
|
||||
|
||||
23
tests/controls/test_icons_helper.py
Normal file
23
tests/controls/test_icons_helper.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from myfasthtml.controls.IconsHelper import IconsHelper
|
||||
|
||||
|
||||
def test_existing_icon():
|
||||
IconsHelper.reset()
|
||||
assert IconsHelper.get(True) is not None
|
||||
|
||||
|
||||
def test_dynamic_icon():
|
||||
IconsHelper.reset()
|
||||
assert IconsHelper.get("add20_filled") is not None
|
||||
|
||||
|
||||
def test_unknown_icon():
|
||||
IconsHelper.reset()
|
||||
assert IconsHelper.get("does_not_exist") is None
|
||||
|
||||
|
||||
|
||||
def test_dynamic_icon_by_package():
|
||||
IconsHelper.reset()
|
||||
assert IconsHelper.get("add20_filled", "fa") is None
|
||||
assert IconsHelper.get("add20_filled", "fluent") is not None
|
||||
@@ -600,7 +600,7 @@ class TestTreeViewRender:
|
||||
- cls "mf-treeview": Root CSS class for TreeView styling
|
||||
"""
|
||||
expected = Div(
|
||||
TestObject(Keyboard, combinations={"esc": TestCommand("CancelRename")}),
|
||||
TestObject(Keyboard, combinations={"esc": {"command": TestCommand("CancelRename"), "require_inside": False}}),
|
||||
_id=tree_view.get_id(),
|
||||
cls="mf-treeview"
|
||||
)
|
||||
@@ -693,7 +693,7 @@ class TestTreeViewRender:
|
||||
child_container = find_one(rendered, Div(data_node_id=child1.id))
|
||||
expected_child_container = Div(
|
||||
Div(
|
||||
Div(None), # No icon, the div is empty
|
||||
None, # No icon for leaf nodes
|
||||
Span("Child1"),
|
||||
Div(), # action buttons
|
||||
cls=Contains("mf-treenode")
|
||||
@@ -721,7 +721,7 @@ class TestTreeViewRender:
|
||||
# Step 2: Define expected structure
|
||||
expected = Div(
|
||||
Div(
|
||||
Div(None), # No icon, the div is empty
|
||||
None, # No icon for leaf nodes
|
||||
Span("Leaf Node"), # Label
|
||||
Div(), # Action buttons still present
|
||||
),
|
||||
@@ -749,7 +749,7 @@ class TestTreeViewRender:
|
||||
|
||||
expected = Div(
|
||||
Div(
|
||||
Div(None), # No icon, leaf node
|
||||
None, # No icon for leaf nodes
|
||||
Span("Selected Node"),
|
||||
Div(), # Action buttons
|
||||
cls=Contains("mf-treenode", "selected")
|
||||
@@ -779,13 +779,13 @@ class TestTreeViewRender:
|
||||
|
||||
expected = Div(
|
||||
Div(
|
||||
Div(None), # No icon, leaf node
|
||||
None, # No icon for leaf nodes
|
||||
Input(
|
||||
name="node_label",
|
||||
value="Edit Me",
|
||||
cls=Contains("mf-treenode-input")
|
||||
),
|
||||
Div(), # Action buttons
|
||||
# Div(), # Action buttons
|
||||
cls=Contains("mf-treenode")
|
||||
),
|
||||
cls="mf-treenode-container",
|
||||
@@ -859,7 +859,7 @@ class TestTreeViewRender:
|
||||
grandchild_container = find_one(rendered, Div(data_node_id=grandchild.id))
|
||||
grandchild_expected = Div(
|
||||
Div(
|
||||
Div(None), # No icon, leaf node
|
||||
None, # No icon for leaf nodes
|
||||
Span("Grandchild"),
|
||||
Div(), # Action buttons
|
||||
cls=Contains("mf-treenode"),
|
||||
@@ -997,7 +997,9 @@ class TestTreeViewRender:
|
||||
keyboard = find_one(rendered, TestObject(Keyboard))
|
||||
|
||||
# Step 2: Define expected structure
|
||||
expected = TestObject(Keyboard, combinations={"esc": TestCommand("CancelRename")})
|
||||
expected = TestObject(Keyboard, combinations={"esc":
|
||||
{"command": TestCommand("CancelRename"),
|
||||
"require_inside": False}})
|
||||
|
||||
# Step 3: Compare
|
||||
assert matches(keyboard, expected)
|
||||
@@ -1026,7 +1028,7 @@ class TestTreeViewRender:
|
||||
|
||||
expected_root1 = Div(
|
||||
Div(
|
||||
Div(None), # No icon, leaf node
|
||||
None, # No icon for leaf nodes
|
||||
Span("Root 1"),
|
||||
Div(), # Action buttons
|
||||
cls=Contains("mf-treenode")
|
||||
@@ -1037,7 +1039,7 @@ class TestTreeViewRender:
|
||||
|
||||
expected_root2 = Div(
|
||||
Div(
|
||||
Div(None), # No icon, leaf node
|
||||
None, # No icon for leaf nodes
|
||||
Span("Root 2"),
|
||||
Div(), # Action buttons
|
||||
cls=Contains("mf-treenode")
|
||||
|
||||
Reference in New Issue
Block a user