diff --git a/src/myfasthtml/assets/myfasthtml.js b/src/myfasthtml/assets/myfasthtml.js index 0f81e4e..7b09487 100644 --- a/src/myfasthtml/assets/myfasthtml.js +++ b/src/myfasthtml/assets/myfasthtml.js @@ -152,7 +152,6 @@ function initLayoutResizer(layoutId) { // Re-initialize after HTMX swaps within this layout layoutElement.addEventListener('htmx:afterSwap', function (event) { - console.log('Layout swapped:', event.detail.target); initResizers(); }); } @@ -213,7 +212,7 @@ function initBoundaries(elementId, updateUrl) { * This function is called when switching between tabs to update both the content visibility * 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) { const controller = document.getElementById(controllerId); @@ -228,8 +227,8 @@ function updateTabs(controllerId) { return; } - // Extract manager ID from controller ID (remove '-content-controller' suffix) - const managerId = controllerId.replace('-content-controller', ''); + // Extract manager ID from controller ID (remove '-controller' suffix) + const managerId = controllerId.replace('-controller', ''); // Hide all tab contents for this manager const contentWrapper = document.getElementById(`${managerId}-content-wrapper`); diff --git a/src/myfasthtml/controls/TabsManager.py b/src/myfasthtml/controls/TabsManager.py index 6b5ec55..eafe18f 100644 --- a/src/myfasthtml/controls/TabsManager.py +++ b/src/myfasthtml/controls/TabsManager.py @@ -60,7 +60,7 @@ class Commands(BaseCommands): def show_tab(self, tab_id): return Command(f"{self._prefix}ShowTab", "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): return Command(f"{self._prefix}CloseTab", @@ -71,20 +71,10 @@ class Commands(BaseCommands): return (Command(f"{self._prefix}AddTab", "Add a new tab", self._owner.on_new_tab, label, component, auto_increment). - htmx(target=f"#{self._id}-content-controller")) - - def update_boundaries(self): - return Command(f"{self._prefix}UpdateBoundaries", - "Update component boundaries", - self._owner.update_boundaries).htmx(target=None) + htmx(target=f"#{self._id}-controller")) 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 def __init__(self, session, _id=None): @@ -264,8 +254,8 @@ class TabsManager(MultipleInstance): def _mk_tabs_controller(self): return Div( - Div(id=f"{self._id}-content-controller", data_active_tab=f"{self._state.active_tab}"), - Script(f'updateTabs("{self._id}-content-controller");'), + Div(id=f"{self._id}-controller", data_active_tab=f"{self._state.active_tab}"), + Script(f'updateTabs("{self._id}-controller");'), ) def _mk_tabs_header(self, oob=False): diff --git a/tests/controls/test_tabsmanager.py b/tests/controls/test_tabsmanager.py index 41d1326..aae41ed 100644 --- a/tests/controls/test_tabsmanager.py +++ b/tests/controls/test_tabsmanager.py @@ -1,9 +1,10 @@ import pytest from fasthtml.components import * +from fasthtml.xtend import Script from myfasthtml.controls.TabsManager import TabsManager 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 @@ -20,11 +21,12 @@ class TestTabsManagerBehaviour: assert from_instance_manager == 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 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_id"] is None # Div is not BaseInstance assert tabs_manager.get_state().tabs_order == [tab_id] @@ -37,34 +39,57 @@ class TestTabsManagerBehaviour: assert len(tabs_manager.get_state().tabs) == 2 assert tabs_manager.get_state().tabs_order == [tab_id1, 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: def test_i_can_render_when_no_tabs(self, tabs_manager): 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( Div( - Button(), - id=f"{tabs_manager.get_id()}-header" + Div(id=f"{tabs_manager.get_id()}-controller"), + Script(f'updateTabs("{tabs_manager.get_id()}-controller");'), ), 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(), ) + assert matches(res, expected) def test_i_can_render_when_multiple_tabs(self, tabs_manager): @@ -75,13 +100,22 @@ class TestTabsManagerRender: expected = Div( Div( - Button(), - Button(), - Button(), - id=f"{tabs_manager.get_id()}-header" + Div(id=f"{tabs_manager.get_id()}-controller"), + Script(f'updateTabs("{tabs_manager.get_id()}-controller");'), ), 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(), )