diff --git a/CHANGELOG.md b/CHANGELOG.md index 7032ca1ee4..6306b7d283 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Fixed duplicated key displays in the help panel https://github.com/Textualize/textual/issues/5037 +- Fixed crash when removing then adding panes to `TabbedContent` https://github.com/Textualize/textual/issues/5215 ## [0.85.2] - 2024-11-02 diff --git a/src/textual/widgets/_tabbed_content.py b/src/textual/widgets/_tabbed_content.py index d216688f7f..6b22492c19 100644 --- a/src/textual/widgets/_tabbed_content.py +++ b/src/textual/widgets/_tabbed_content.py @@ -332,6 +332,8 @@ def __init__( self.titles = [self.render_str(title) for title in titles] self._tab_content: list[Widget] = [] self._initial = initial + self._cumulative_tab_count = 0 + """Tracks the total number of tabs added to ensure unique IDs if tabs are removed""" super().__init__(name=name, id=id, classes=classes, disabled=disabled) @property @@ -384,6 +386,8 @@ def compose(self) -> ComposeResult: for content in pane_content ] + self._cumulative_tab_count = len(tabs) + # Yield the tabs, and ensure they're linked to this TabbedContent. # It's important to associate the Tabs with the TabbedContent, so that this # TabbedContent can determine whether a message received from a Tabs instance @@ -419,12 +423,13 @@ def add_pane( Only one of `before` or `after` can be provided. If both are provided an exception is raised. """ + self._cumulative_tab_count += 1 if isinstance(before, TabPane): before = before.id if isinstance(after, TabPane): after = after.id tabs = self.get_child_by_type(ContentTabs) - pane = self._set_id(pane, tabs.tab_count + 1) + pane = self._set_id(pane, self._cumulative_tab_count) assert pane.id is not None pane.display = False return AwaitComplete( diff --git a/tests/test_tabbed_content.py b/tests/test_tabbed_content.py index 57a8874ebf..06c4693138 100644 --- a/tests/test_tabbed_content.py +++ b/tests/test_tabbed_content.py @@ -914,3 +914,23 @@ def compose(self) -> ComposeResult: await pilot.pause() assert app.query_one("Tab#duplicate").disabled is True assert app.query_one("TabPane#duplicate").disabled is False + + +async def test_remove_and_add_pane_no_duplicate_id_error(): + """Ensure that removing then adding panes does not raise a `DuplicateIds` exception. + + Regression test for https://github.com/Textualize/textual/issues/5215 + """ + + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + yield Label("tab-2") + + app = TabbedApp() + async with app.run_test() as pilot: + # If no exception is raised, the test will pass + tabbed_content = pilot.app.query_one(TabbedContent) + await tabbed_content.remove_pane("tab-1") + await tabbed_content.add_pane(TabPane("Tab 3", Label("tab-3")))