Added first version of tab management

This commit is contained in:
2025-11-16 15:29:56 +01:00
parent c38a012c74
commit ca238303b8
3 changed files with 64 additions and 41 deletions

View File

@@ -152,7 +152,6 @@ function initLayoutResizer(layoutId) {
// Re-initialize after HTMX swaps within this layout // Re-initialize after HTMX swaps within this layout
layoutElement.addEventListener('htmx:afterSwap', function (event) { layoutElement.addEventListener('htmx:afterSwap', function (event) {
console.log('Layout swapped:', event.detail.target);
initResizers(); initResizers();
}); });
} }
@@ -213,7 +212,7 @@ function initBoundaries(elementId, updateUrl) {
* This function is called when switching between tabs to update both the content visibility * This function is called when switching between tabs to update both the content visibility
* and the tab button states. * and the tab button states.
* *
* @param {string} controllerId - The ID of the tabs controller element (format: "{managerId}-content-controller") * @param {string} controllerId - The ID of the tabs controller element (format: "{managerId}-controller")
*/ */
function updateTabs(controllerId) { function updateTabs(controllerId) {
const controller = document.getElementById(controllerId); const controller = document.getElementById(controllerId);
@@ -228,8 +227,8 @@ function updateTabs(controllerId) {
return; return;
} }
// Extract manager ID from controller ID (remove '-content-controller' suffix) // Extract manager ID from controller ID (remove '-controller' suffix)
const managerId = controllerId.replace('-content-controller', ''); const managerId = controllerId.replace('-controller', '');
// Hide all tab contents for this manager // Hide all tab contents for this manager
const contentWrapper = document.getElementById(`${managerId}-content-wrapper`); const contentWrapper = document.getElementById(`${managerId}-content-wrapper`);

View File

@@ -60,7 +60,7 @@ class Commands(BaseCommands):
def show_tab(self, tab_id): def show_tab(self, tab_id):
return Command(f"{self._prefix}ShowTab", return Command(f"{self._prefix}ShowTab",
"Activate or show a specific tab", "Activate or show a specific tab",
self._owner.show_tab, tab_id).htmx(target=f"#{self._id}-content-controller", swap="outerHTML") self._owner.show_tab, tab_id).htmx(target=f"#{self._id}-controller", swap="outerHTML")
def close_tab(self, tab_id): def close_tab(self, tab_id):
return Command(f"{self._prefix}CloseTab", return Command(f"{self._prefix}CloseTab",
@@ -71,20 +71,10 @@ class Commands(BaseCommands):
return (Command(f"{self._prefix}AddTab", return (Command(f"{self._prefix}AddTab",
"Add a new tab", "Add a new tab",
self._owner.on_new_tab, label, component, auto_increment). self._owner.on_new_tab, label, component, auto_increment).
htmx(target=f"#{self._id}-content-controller")) htmx(target=f"#{self._id}-controller"))
def update_boundaries(self):
return Command(f"{self._prefix}UpdateBoundaries",
"Update component boundaries",
self._owner.update_boundaries).htmx(target=None)
class TabsManager(MultipleInstance): class TabsManager(MultipleInstance):
# Constants for width calculation
TAB_CHAR_WIDTH = 8 # pixels per character
TAB_PADDING = 40 # padding + close button space
TAB_MIN_WIDTH = 80 # minimum tab width
DROPDOWN_BTN_WIDTH = 40 # width of the dropdown button
_tab_count = 0 _tab_count = 0
def __init__(self, session, _id=None): def __init__(self, session, _id=None):
@@ -264,8 +254,8 @@ class TabsManager(MultipleInstance):
def _mk_tabs_controller(self): def _mk_tabs_controller(self):
return Div( return Div(
Div(id=f"{self._id}-content-controller", data_active_tab=f"{self._state.active_tab}"), Div(id=f"{self._id}-controller", data_active_tab=f"{self._state.active_tab}"),
Script(f'updateTabs("{self._id}-content-controller");'), Script(f'updateTabs("{self._id}-controller");'),
) )
def _mk_tabs_header(self, oob=False): def _mk_tabs_header(self, oob=False):

View File

@@ -1,9 +1,10 @@
import pytest import pytest
from fasthtml.components import * from fasthtml.components import *
from fasthtml.xtend import Script
from myfasthtml.controls.TabsManager import TabsManager from myfasthtml.controls.TabsManager import TabsManager
from myfasthtml.core.instances import InstancesManager from myfasthtml.core.instances import InstancesManager
from myfasthtml.test.matcher import matches, NoChildren from myfasthtml.test.matcher import matches, NoChildren, StartsWith
from .conftest import session from .conftest import session
@@ -20,11 +21,12 @@ class TestTabsManagerBehaviour:
assert from_instance_manager == tabs_manager assert from_instance_manager == tabs_manager
def test_i_can_add_tab(self, tabs_manager): def test_i_can_add_tab(self, tabs_manager):
tab_id = tabs_manager.add_tab("Users", Div("Content 1")) tab_id = tabs_manager.add_tab("Tab1", Div("Content 1"))
assert tab_id is not None assert tab_id is not None
assert tab_id in tabs_manager.get_state().tabs assert tab_id in tabs_manager.get_state().tabs
assert tabs_manager.get_state().tabs[tab_id]["label"] == "Users" assert tabs_manager.get_state().tabs[tab_id]["label"] == "Tab1"
assert tabs_manager.get_state().tabs[tab_id]["id"] == tab_id
assert tabs_manager.get_state().tabs[tab_id]["component_type"] is None # Div is not BaseInstance assert tabs_manager.get_state().tabs[tab_id]["component_type"] is None # Div is not BaseInstance
assert tabs_manager.get_state().tabs[tab_id]["component_id"] is None # Div is not BaseInstance assert tabs_manager.get_state().tabs[tab_id]["component_id"] is None # Div is not BaseInstance
assert tabs_manager.get_state().tabs_order == [tab_id] assert tabs_manager.get_state().tabs_order == [tab_id]
@@ -37,34 +39,57 @@ class TestTabsManagerBehaviour:
assert len(tabs_manager.get_state().tabs) == 2 assert len(tabs_manager.get_state().tabs) == 2
assert tabs_manager.get_state().tabs_order == [tab_id1, tab_id2] assert tabs_manager.get_state().tabs_order == [tab_id1, tab_id2]
assert tabs_manager.get_state().active_tab == tab_id2 assert tabs_manager.get_state().active_tab == tab_id2
def test_i_can_show_tab(self, tabs_manager):
tab_id1 = tabs_manager.add_tab("Tab1", Div("Content 1"))
tab_id2 = tabs_manager.add_tab("Tab2", Div("Content 2"))
assert tabs_manager.get_state().active_tab == tab_id2 # last crated tab is active
tabs_manager.show_tab(tab_id1)
assert tabs_manager.get_state().active_tab == tab_id1
def test_i_can_close_tab(self, tabs_manager):
tab_id1 = tabs_manager.add_tab("Tab1", Div("Content 1"))
tab_id2 = tabs_manager.add_tab("Tab2", Div("Content 2"))
tab_id3 = tabs_manager.add_tab("Tab3", Div("Content 3"))
tabs_manager.close_tab(tab_id2)
assert len(tabs_manager.get_state().tabs) == 2
assert [tab_id for tab_id in tabs_manager.get_state().tabs] == [tab_id1, tab_id3]
assert tabs_manager.get_state().tabs_order == [tab_id1, tab_id3]
assert tabs_manager.get_state().active_tab == tab_id3 # last tab stays active
def test_i_still_have_an_active_tab_after_close(self, tabs_manager):
tab_id1 = tabs_manager.add_tab("Tab1", Div("Content 1"))
tab_id2 = tabs_manager.add_tab("Tab2", Div("Content 2"))
tab_id3 = tabs_manager.add_tab("Tab3", Div("Content 3"))
tabs_manager.close_tab(tab_id3) # close the currently active tab
assert tabs_manager.get_state().active_tab == tab_id1 # default to the first tab
class TestTabsManagerRender: class TestTabsManagerRender:
def test_i_can_render_when_no_tabs(self, tabs_manager): def test_i_can_render_when_no_tabs(self, tabs_manager):
res = tabs_manager.render() res = tabs_manager.render()
expected = Div(
Div(NoChildren(), id=f"{tabs_manager.get_id()}-header"),
Div(id=f"{tabs_manager.get_id()}-content"),
id=tabs_manager.get_id(),
)
assert matches(res, expected)
def test_i_can_render_when_one_tab(self, tabs_manager):
tabs_manager.add_tab("Users", Div("Content 1"))
res = tabs_manager.render()
expected = Div( expected = Div(
Div( Div(
Button(), Div(id=f"{tabs_manager.get_id()}-controller"),
id=f"{tabs_manager.get_id()}-header" Script(f'updateTabs("{tabs_manager.get_id()}-controller");'),
), ),
Div( Div(
Div("Content 1") Div(NoChildren(), id=f"{tabs_manager.get_id()}-header"),
id=f"{tabs_manager.get_id()}-header-wrapper"
),
Div(
Div(id=f"{tabs_manager.get_id()}-None-content"),
id=f"{tabs_manager.get_id()}-content-wrapper"
), ),
id=tabs_manager.get_id(), id=tabs_manager.get_id(),
) )
assert matches(res, expected) assert matches(res, expected)
def test_i_can_render_when_multiple_tabs(self, tabs_manager): def test_i_can_render_when_multiple_tabs(self, tabs_manager):
@@ -75,13 +100,22 @@ class TestTabsManagerRender:
expected = Div( expected = Div(
Div( Div(
Button(), Div(id=f"{tabs_manager.get_id()}-controller"),
Button(), Script(f'updateTabs("{tabs_manager.get_id()}-controller");'),
Button(),
id=f"{tabs_manager.get_id()}-header"
), ),
Div( Div(
Div("Content 3") Div(
Div(), # tab_button
Div(), # tab_button
Div(), # tab_button
id=f"{tabs_manager.get_id()}-header"
),
id=f"{tabs_manager.get_id()}-header-wrapper"
),
Div(
Div(id=StartsWith(tabs_manager.get_id())),
# Lasy loading for the other contents
id=f"{tabs_manager.get_id()}-content-wrapper"
), ),
id=tabs_manager.get_id(), id=tabs_manager.get_id(),
) )