Skip to content

Commit 4b02d37

Browse files
davepwillmcgugan
andauthored
TabbedContent remove pane fix (actually Tabs fix) (#2808)
* Add a unit test for #2807 * Add a test for removing tabs in reverse * Add a test for the messages sent when removing tabs in reverse Marked as xfail for the moment, I suspect the root cause of #2807. * Don't sent Changed when tab removal doesn't result in change * Update the CHANGELOG --------- Co-authored-by: Will McGugan <[email protected]>
1 parent 9639449 commit 4b02d37

File tree

4 files changed

+95
-9
lines changed

4 files changed

+95
-9
lines changed

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1212
- Fixed indented code blocks not showing up in `Markdown` https://github.com/Textualize/textual/issues/2781
1313
- Fixed inline code blocks in lists showing out of order in `Markdown` https://github.com/Textualize/textual/issues/2676
1414
- Fixed list items in a `Markdown` being added to the focus chain https://github.com/Textualize/textual/issues/2380
15+
- Fixed `Tabs` posting unnecessary messages when removing non-active tabs https://github.com/Textualize/textual/issues/2807
16+
- call_after_refresh will preserve the sender within the callback https://github.com/Textualize/textual/pull/2806
17+
1518

1619
### Added
1720

@@ -22,9 +25,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2225

2326
- Tooltips are now inherited, so will work with compound widgets
2427

25-
### Fixed
26-
27-
- call_after_refresh will preserve the sender within the callback https://github.com/Textualize/textual/pull/2806
2828

2929
## [0.28.0] - 2023-06-19
3030

src/textual/widgets/_tabs.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -429,11 +429,11 @@ def remove_tab(self, tab_or_id: Tab | str | None) -> AwaitRemove:
429429
removing_active_tab = remove_tab.has_class("-active")
430430

431431
next_tab = self._next_active
432-
result_message: Tabs.Cleared | Tabs.TabActivated = (
433-
self.Cleared(self)
434-
if next_tab is None
435-
else self.TabActivated(self, next_tab)
436-
)
432+
result_message: Tabs.Cleared | Tabs.TabActivated | None = None
433+
if removing_active_tab and next_tab is not None:
434+
result_message = self.TabActivated(self, next_tab)
435+
elif self.tab_count == 1:
436+
result_message = self.Cleared(self)
437437

438438
remove_await = remove_tab.remove()
439439

@@ -444,7 +444,8 @@ async def do_remove() -> None:
444444
if next_tab is not None:
445445
next_tab.add_class("-active")
446446
self.call_after_refresh(self._highlight_active, animate=True)
447-
self.post_message(result_message)
447+
if result_message is not None:
448+
self.post_message(result_message)
448449

449450
self.call_after_refresh(do_remove)
450451

tests/test_tabbed_content.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,41 @@ def on_tabbed_content_cleared(self) -> None:
369369
assert tabbed_content.active == ""
370370

371371

372+
async def test_tabbed_content_reversed_removal():
373+
class TabbedApp(App[None]):
374+
cleared: var[int] = var(0)
375+
376+
def compose(self) -> ComposeResult:
377+
with TabbedContent():
378+
yield TabPane("Test 1", id="initial-1")
379+
yield TabPane("Test 2", id="initial-2")
380+
yield TabPane("Test 3", id="initial-3")
381+
382+
def on_tabbed_content_cleared(self) -> None:
383+
self.cleared += 1
384+
385+
async with TabbedApp().run_test() as pilot:
386+
tabbed_content = pilot.app.query_one(TabbedContent)
387+
assert tabbed_content.tab_count == 3
388+
assert pilot.app.cleared == 0
389+
assert tabbed_content.active == "initial-1"
390+
await tabbed_content.remove_pane("initial-3")
391+
await pilot.pause()
392+
assert tabbed_content.tab_count == 2
393+
assert pilot.app.cleared == 0
394+
assert tabbed_content.active == "initial-1"
395+
await tabbed_content.remove_pane("initial-2")
396+
await pilot.pause()
397+
assert tabbed_content.tab_count == 1
398+
assert pilot.app.cleared == 0
399+
assert tabbed_content.active == "initial-1"
400+
await tabbed_content.remove_pane("initial-1")
401+
await pilot.pause()
402+
assert tabbed_content.tab_count == 0
403+
assert pilot.app.cleared == 1
404+
assert tabbed_content.active == ""
405+
406+
372407
async def test_tabbed_content_clear():
373408
class TabbedApp(App[None]):
374409
cleared: var[int] = var(0)

tests/test_tabs.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,43 @@ def compose(self) -> ComposeResult:
243243
assert tabs.active_tab is None
244244

245245

246+
async def test_remove_tabs_reversed():
247+
"""It should be possible to remove tabs."""
248+
249+
class TabsApp(App[None]):
250+
def compose(self) -> ComposeResult:
251+
yield Tabs("John", "Aeryn", "Moya", "Pilot")
252+
253+
async with TabsApp().run_test() as pilot:
254+
tabs = pilot.app.query_one(Tabs)
255+
assert tabs.tab_count == 4
256+
assert tabs.active_tab is not None
257+
assert tabs.active_tab.id == "tab-1"
258+
259+
await tabs.remove_tab("tab-4")
260+
await pilot.pause()
261+
assert tabs.tab_count == 3
262+
assert tabs.active_tab is not None
263+
assert tabs.active_tab.id == "tab-1"
264+
265+
await tabs.remove_tab("tab-3")
266+
await pilot.pause()
267+
assert tabs.tab_count == 2
268+
assert tabs.active_tab is not None
269+
assert tabs.active_tab.id == "tab-1"
270+
271+
await tabs.remove_tab("tab-2")
272+
await pilot.pause()
273+
assert tabs.tab_count == 1
274+
assert tabs.active_tab is not None
275+
assert tabs.active_tab.id == "tab-1"
276+
277+
await tabs.remove_tab("tab-1")
278+
await pilot.pause()
279+
assert tabs.tab_count == 0
280+
assert tabs.active_tab is None
281+
282+
246283
async def test_clear_tabs():
247284
"""It should be possible to clear all tabs."""
248285

@@ -420,6 +457,19 @@ async def test_remove_tabs_messages():
420457
]
421458

422459

460+
async def test_reverse_remove_tabs_messages():
461+
"""Removing tabs should result in various messages."""
462+
async with TabsMessageCatchApp().run_test() as pilot:
463+
tabs = pilot.app.query_one(Tabs)
464+
for n in reversed(range(4)):
465+
await tabs.remove_tab(f"tab-{n+1}")
466+
await pilot.pause()
467+
assert pilot.app.intended_handlers == [
468+
"on_tabs_tab_activated",
469+
"on_tabs_cleared",
470+
]
471+
472+
423473
async def test_keyboard_navigation_messages():
424474
"""Keyboard navigation should result in the expected messages."""
425475
async with TabsMessageCatchApp().run_test() as pilot:

0 commit comments

Comments
 (0)