@@ -37,6 +37,26 @@ def __init__(self, label: Text, content_id: str, disabled: bool = False):
3737 super ().__init__ (label , id = content_id , disabled = disabled )
3838
3939
40+ class ContentTabs (Tabs ):
41+ """A Tabs which is associated with a TabbedContent."""
42+
43+ def __init__ (
44+ self ,
45+ * tabs : Tab | TextType ,
46+ active : str | None = None ,
47+ tabbed_content : TabbedContent ,
48+ ):
49+ """Initialize a ContentTabs.
50+
51+ Args:
52+ *tabs: The child tabs.
53+ active: ID of the tab which should be active on start.
54+ tabbed_content: The associated TabbedContent instance.
55+ """
56+ super ().__init__ (* tabs , active = active )
57+ self .tabbed_content = tabbed_content
58+
59+
4060class TabPane (Widget ):
4161 """A container for switchable content, with additional title.
4262
@@ -245,11 +265,21 @@ def compose(self) -> ComposeResult:
245265 ]
246266 # Get a tab for each pane
247267 tabs = [
248- ContentTab (content ._title , content .id or "" , disabled = content .disabled )
268+ ContentTab (
269+ content ._title ,
270+ content .id or "" ,
271+ disabled = content .disabled ,
272+ )
249273 for content in pane_content
250274 ]
251- # Yield the tabs
252- yield Tabs (* tabs , active = self ._initial or None )
275+
276+ # Yield the tabs, and ensure they're linked to this TabbedContent.
277+ # It's important to associate the Tabs with the TabbedContent, so that this
278+ # TabbedContent can determine whether a message received from a Tabs instance
279+ # has been sent from this Tabs, or from a Tabs that may exist as a descendant
280+ # deeper in the DOM.
281+ yield ContentTabs (* tabs , active = self ._initial or None , tabbed_content = self )
282+
253283 # Yield the content switcher and panes
254284 with ContentSwitcher (initial = self ._initial or None ):
255285 yield from pane_content
@@ -282,7 +312,7 @@ def add_pane(
282312 before = before .id
283313 if isinstance (after , TabPane ):
284314 after = after .id
285- tabs = self .get_child_by_type (Tabs )
315+ tabs = self .get_child_by_type (ContentTabs )
286316 pane = self ._set_id (pane , tabs .tab_count + 1 )
287317 assert pane .id is not None
288318 pane .display = False
@@ -301,7 +331,7 @@ def remove_pane(self, pane_id: str) -> AwaitComplete:
301331 An optionally awaitable object that waits for the pane to be removed
302332 and the Cleared message to be posted.
303333 """
304- removal_awaitables = [self .get_child_by_type (Tabs ).remove_tab (pane_id )]
334+ removal_awaitables = [self .get_child_by_type (ContentTabs ).remove_tab (pane_id )]
305335 try :
306336 removal_awaitables .append (
307337 self .get_child_by_type (ContentSwitcher )
@@ -332,7 +362,7 @@ def clear_panes(self) -> AwaitComplete:
332362 and the Cleared message to be posted.
333363 """
334364 await_clear = gather (
335- self .get_child_by_type (Tabs ).clear (),
365+ self .get_child_by_type (ContentTabs ).clear (),
336366 self .get_child_by_type (ContentSwitcher ).remove_children (),
337367 )
338368
@@ -356,35 +386,52 @@ def compose_add_child(self, widget: Widget) -> None:
356386
357387 def _on_tabs_tab_activated (self , event : Tabs .TabActivated ) -> None :
358388 """User clicked a tab."""
359- assert isinstance (event .tab , ContentTab )
360- assert isinstance (event .tab .id , str )
361- event .stop ()
362- switcher = self .get_child_by_type (ContentSwitcher )
363- switcher .current = event .tab .id
364- self .active = event .tab .id
365- self .post_message (
366- TabbedContent .TabActivated (
367- tabbed_content = self ,
368- tab = event .tab ,
389+ if self ._is_associated_tabs (event .tabs ):
390+ # The message is relevant, so consume it and update state accordingly.
391+ event .stop ()
392+ switcher = self .get_child_by_type (ContentSwitcher )
393+ switcher .current = event .tab .id
394+ self .active = event .tab .id
395+ self .post_message (
396+ TabbedContent .TabActivated (
397+ tabbed_content = self ,
398+ tab = event .tab ,
399+ )
369400 )
370- )
371401
372402 def _on_tabs_cleared (self , event : Tabs .Cleared ) -> None :
373- """All tabs were removed."""
374- event .stop ()
375- self .get_child_by_type (ContentSwitcher ).current = None
376- self .active = ""
403+ """Called when there are no active tabs. The tabs may have been cleared,
404+ or they may all be hidden."""
405+ if self ._is_associated_tabs (event .tabs ):
406+ event .stop ()
407+ self .get_child_by_type (ContentSwitcher ).current = None
408+ self .active = ""
409+
410+ def _is_associated_tabs (self , tabs : Tabs ) -> bool :
411+ """Determine whether a tab is associated with this TabbedContent or not.
412+
413+ A tab is "associated" with a `TabbedContent`, if it's one of the tabs that can
414+ be used to control it. These have a special type: `ContentTab`, and are linked
415+ back to this `TabbedContent` instance via a `tabbed_content` attribute.
416+
417+ Args:
418+ tabs: The Tabs instance to check.
419+
420+ Returns:
421+ True if the tab is associated with this `TabbedContent`.
422+ """
423+ return isinstance (tabs , ContentTabs ) and tabs .tabbed_content is self
377424
378425 def _watch_active (self , active : str ) -> None :
379426 """Switch tabs when the active attributes changes."""
380427 with self .prevent (Tabs .TabActivated ):
381- self .get_child_by_type (Tabs ).active = active
428+ self .get_child_by_type (ContentTabs ).active = active
382429 self .get_child_by_type (ContentSwitcher ).current = active
383430
384431 @property
385432 def tab_count (self ) -> int :
386433 """Total number of tabs."""
387- return self .get_child_by_type (Tabs ).tab_count
434+ return self .get_child_by_type (ContentTabs ).tab_count
388435
389436 def _on_tabs_tab_disabled (self , event : Tabs .TabDisabled ) -> None :
390437 """Disable the corresponding tab pane."""
@@ -404,7 +451,7 @@ def _on_tab_pane_disabled(self, event: TabPane.Disabled) -> None:
404451 tab_pane_id = event .tab_pane .id or ""
405452 try :
406453 with self .prevent (Tab .Disabled ):
407- self .get_child_by_type (Tabs ).query_one (
454+ self .get_child_by_type (ContentTabs ).query_one (
408455 f"Tab#{ tab_pane_id } "
409456 ).disabled = True
410457 except NoMatches :
@@ -428,7 +475,7 @@ def _on_tab_pane_enabled(self, event: TabPane.Enabled) -> None:
428475 tab_pane_id = event .tab_pane .id or ""
429476 try :
430477 with self .prevent (Tab .Enabled ):
431- self .get_child_by_type (Tabs ).query_one (
478+ self .get_child_by_type (ContentTabs ).query_one (
432479 f"Tab#{ tab_pane_id } "
433480 ).disabled = False
434481 except NoMatches :
@@ -444,7 +491,7 @@ def disable_tab(self, tab_id: str) -> None:
444491 Tabs.TabError: If there are any issues with the request.
445492 """
446493
447- self .get_child_by_type (Tabs ).disable (tab_id )
494+ self .get_child_by_type (ContentTabs ).disable (tab_id )
448495
449496 def enable_tab (self , tab_id : str ) -> None :
450497 """Enables the tab with the given ID.
@@ -456,7 +503,7 @@ def enable_tab(self, tab_id: str) -> None:
456503 Tabs.TabError: If there are any issues with the request.
457504 """
458505
459- self .get_child_by_type (Tabs ).enable (tab_id )
506+ self .get_child_by_type (ContentTabs ).enable (tab_id )
460507
461508 def hide_tab (self , tab_id : str ) -> None :
462509 """Hides the tab with the given ID.
@@ -468,7 +515,7 @@ def hide_tab(self, tab_id: str) -> None:
468515 Tabs.TabError: If there are any issues with the request.
469516 """
470517
471- self .get_child_by_type (Tabs ).hide (tab_id )
518+ self .get_child_by_type (ContentTabs ).hide (tab_id )
472519
473520 def show_tab (self , tab_id : str ) -> None :
474521 """Shows the tab with the given ID.
@@ -480,4 +527,4 @@ def show_tab(self, tab_id: str) -> None:
480527 Tabs.TabError: If there are any issues with the request.
481528 """
482529
483- self .get_child_by_type (Tabs ).show (tab_id )
530+ self .get_child_by_type (ContentTabs ).show (tab_id )
0 commit comments