diff --git a/CHANGELOG.md b/CHANGELOG.md index 846f70b2f4..67c010193f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed infinite loop in `Widget.anchor` https://github.com/Textualize/textual/pull/5290 - Restores the ability to supply console markup to command list https://github.com/Textualize/textual/pull/5294 - Fixed delayed App Resize event https://github.com/Textualize/textual/pull/5296 +- Fixed issue with auto-generated tab IDs https://github.com/Textualize/textual/pull/5298 ## [0.87.1] - 2024-11-24 diff --git a/src/textual/widgets/_tabbed_content.py b/src/textual/widgets/_tabbed_content.py index 37630b05dc..2d625e7cd0 100644 --- a/src/textual/widgets/_tabbed_content.py +++ b/src/textual/widgets/_tabbed_content.py @@ -332,6 +332,7 @@ def __init__( self.titles = [self.render_str(title) for title in titles] self._tab_content: list[Widget] = [] self._initial = initial + self._tab_counter = 0 super().__init__(name=name, id=id, classes=classes, disabled=disabled) @property @@ -357,6 +358,15 @@ def _set_id(content: TabPane, new_id: int) -> TabPane: content.id = f"tab-{new_id}" return content + def _generate_tab_id(self) -> int: + """Auto generate a new tab id. + + Returns: + An auto-incrementing integer. + """ + self._tab_counter += 1 + return self._tab_counter + def compose(self) -> ComposeResult: """Compose the tabbed content.""" @@ -368,7 +378,7 @@ def compose(self) -> ComposeResult: if isinstance(content, TabPane) else TabPane(title or self.render_str(f"Tab {index}"), content) ), - index, + self._generate_tab_id(), ) for index, (title, content) in enumerate( zip_longest(self.titles, self._tab_content), 1 @@ -424,7 +434,7 @@ def add_pane( 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._generate_tab_id()) assert pane.id is not None pane.display = False return AwaitComplete( diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_add_remove_tabs.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_add_remove_tabs.svg new file mode 100644 index 0000000000..7f987a7129 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_add_remove_tabs.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ExampleApp + + + + + + + + + + tab-2New tabNew tab +━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +tab-2                                                                            + + + + + + + + + + + + + + + + + + + + + r Remove first pane  a Add pane                                    ^p palette + + + diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index b2fcd41a1d..a6f3a8e36d 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -35,6 +35,8 @@ Tab, Tabs, TextArea, + TabbedContent, + TabPane, ) from textual.widgets.text_area import BUILTIN_LANGUAGES, Selection, TextAreaTheme from textual.theme import Theme @@ -2752,7 +2754,6 @@ async def run_before(pilot: Pilot) -> None: snap_compare(TallSelectApp(), run_before=run_before) - def test_markup_command_list(snap_compare): """Regression test for https://github.com/Textualize/textual/issues/5276 You should see a command list, with console markup applied to the action name and help text.""" @@ -2769,6 +2770,7 @@ def on_mount(self) -> None: snap_compare(MyApp()) + def test_app_resize_order(snap_compare): """Regression test for https://github.com/Textualize/textual/issues/5284 You should see a placeholder with text "BAR", focused and scrolled down so it fills the screen. @@ -2810,3 +2812,32 @@ def on_resize(self) -> None: snap_compare(SCApp()) + +def test_add_remove_tabs(snap_compare): + """Regression test for https://github.com/Textualize/textual/issues/5215 + You should see a TabbedContent with three panes, entitled 'tab-2', 'New tab' and 'New tab'""" + + class ExampleApp(App): + BINDINGS = [ + ("r", "remove_pane", "Remove first pane"), + ("a", "add_pane", "Add pane"), + ] + + def compose(self) -> ComposeResult: + with TabbedContent(initial="tab-2"): + with TabPane("tab-1"): + yield Label("tab-1") + with TabPane("tab-2"): + yield Label("tab-2") + yield Footer() + + def action_remove_pane(self) -> None: + tabbed_content = self.query_one(TabbedContent) + tabbed_content.remove_pane("tab-1") + + def action_add_pane(self) -> None: + tabbed_content = self.query_one(TabbedContent) + new_pane = TabPane("New tab", Label("new")) + tabbed_content.add_pane(new_pane) + + snap_compare(ExampleApp(), press=["a", "r", "a"])