Skip to content

Commit 764f1fb

Browse files
authored
Merge pull request #4159 from davep/tabbed-content-active-issue
Fix active tab not coming into view plus `TabbedContent.TabActivated` not always been posted
2 parents ae35fd5 + 8050acd commit 764f1fb

File tree

4 files changed

+36
-9
lines changed

4 files changed

+36
-9
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1414
- Add undo and redo to TextArea https://github.com/Textualize/textual/pull/4124
1515
- Added support for command palette command discoverability https://github.com/Textualize/textual/pull/4154
1616

17+
### Fixed
18+
19+
- Fixed out-of-view `Tab` not being scrolled into view when `Tabs.active` is assigned https://github.com/Textualize/textual/issues/4150
20+
- Fixed `TabbedContent.TabActivate` not being posted when `TabbedContent.active` is assigned https://github.com/Textualize/textual/issues/4150
21+
1722
### Changed
1823

1924
- Breaking change: Renamed `TextArea.tab_behaviour` to `TextArea.tab_behavior` https://github.com/Textualize/textual/pull/4124

src/textual/widgets/_tabbed_content.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -511,13 +511,22 @@ def _on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None:
511511
if self._is_associated_tabs(event.tabs):
512512
# The message is relevant, so consume it and update state accordingly.
513513
event.stop()
514+
assert event.tab.id is not None
514515
switcher = self.get_child_by_type(ContentSwitcher)
515516
switcher.current = ContentTab.sans_prefix(event.tab.id)
516-
self.active = ContentTab.sans_prefix(event.tab.id)
517+
with self.prevent(self.TabActivated):
518+
# We prevent TabbedContent.TabActivated because it is also
519+
# posted from the watcher for active, we're also about to
520+
# post it below too, which is valid as here we're reacting
521+
# to what the Tabs are doing. This ensures we don't get
522+
# doubled-up messages.
523+
self.active = ContentTab.sans_prefix(event.tab.id)
517524
self.post_message(
518525
TabbedContent.TabActivated(
519526
tabbed_content=self,
520-
tab=event.tab,
527+
tab=self.get_child_by_type(ContentTabs).get_content_tab(
528+
self.active
529+
),
521530
)
522531
)
523532

@@ -548,7 +557,14 @@ def _watch_active(self, active: str) -> None:
548557
"""Switch tabs when the active attributes changes."""
549558
with self.prevent(Tabs.TabActivated):
550559
self.get_child_by_type(ContentTabs).active = ContentTab.add_prefix(active)
551-
self.get_child_by_type(ContentSwitcher).current = active
560+
self.get_child_by_type(ContentSwitcher).current = active
561+
if active:
562+
self.post_message(
563+
TabbedContent.TabActivated(
564+
tabbed_content=self,
565+
tab=self.get_child_by_type(ContentTabs).get_content_tab(active),
566+
)
567+
)
552568

553569
@property
554570
def tab_count(self) -> int:

src/textual/widgets/_tabs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,7 @@ def watch_active(self, previously_active: str, active: str) -> None:
583583
self.query("#tabs-list > Tab.-active").remove_class("-active")
584584
active_tab.add_class("-active")
585585
self._highlight_active(animate=previously_active != "")
586+
self._scroll_active_tab()
586587
self.post_message(self.TabActivated(self, active_tab))
587588
else:
588589
underline = self.query_one(Underline)

tests/test_tabbed_content.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import pytest
24

35
from textual.app import App, ComposeResult
@@ -137,13 +139,12 @@ def compose(self) -> ComposeResult:
137139

138140
async def test_tabbed_content_messages():
139141
class TabbedApp(App):
140-
message = None
142+
activation_history: list[Tab] = []
141143

142144
def compose(self) -> ComposeResult:
143-
with TabbedContent(initial="bar"):
145+
with TabbedContent():
144146
with TabPane("foo", id="foo"):
145147
yield Label("Foo", id="foo-label")
146-
147148
with TabPane("bar", id="bar"):
148149
yield Label("Bar", id="bar-label")
149150
with TabPane("baz", id="baz"):
@@ -152,15 +153,19 @@ def compose(self) -> ComposeResult:
152153
def on_tabbed_content_tab_activated(
153154
self, event: TabbedContent.TabActivated
154155
) -> None:
155-
self.message = event
156+
self.activation_history.append(event.tab)
156157

157158
app = TabbedApp()
158159
async with app.run_test() as pilot:
159160
tabbed_content = app.query_one(TabbedContent)
160161
tabbed_content.active = "bar"
161162
await pilot.pause()
162-
assert isinstance(app.message, TabbedContent.TabActivated)
163-
assert app.message.tab.label.plain == "bar"
163+
assert app.activation_history == [
164+
# foo was originally activated.
165+
app.query_one(TabbedContent).get_tab("foo"),
166+
# then we did bar "by hand"
167+
app.query_one(TabbedContent).get_tab("bar"),
168+
]
164169

165170

166171
async def test_tabbed_content_add_later_from_empty():

0 commit comments

Comments
 (0)