Repository: textualize/textual
Issue: It's possible to somehow break the working of `TabbedContent`

This one isn't exactly easy to explain, but hopefully easy to see and recreate with the following code (which is a distilled version of what I'm doing in a bigger application, where I found it).

Worth noting: the motivation here is that (in the application I'm working on) the `TabbedContent` is acting as a sidebar, where each pane has content that can be focused, and I want folk to be people to switch tabs without needing to navigate to the tab bar and back into the pane again. As such there's some bindings in place that call into the `Tabs` and uses their prev/next tab actions.

```python
from textual.app        import App, ComposeResult
from textual.binding    import Binding
from textual.containers import Vertical
from textual.widgets    import Header, Footer, TabbedContent, TabPane, Tabs, DirectoryTree

class SelfFocusPane(TabPane):

    DEFAULT_CSS = """
    SelfFocusPane {
        height: 100% !important;
    }
    DirectoryTree {
        width: 100%;
        height: 100% !important;
    }
    """

    def compose( self ) -> ComposeResult:
        """Compose the child widgets."""
        yield DirectoryTree(".")

    def on_show( self ) -> None:
        self.query_one( DirectoryTree ).focus()

class TabbedContentIssueApp( App[ None ] ):

    CSS = """
    Screen {
        align: center middle;
    }

    Screen > Vertical {
        width: 42;
    }

    TabbedContent {
        border: round red;
        max-width: 40;
        height: 100%;
    }

    ContentSwitcher {
        height: 1fr !important;
    }
    """

    BINDINGS = [
        Binding( "shift+left", "previous", "Previous" ),
        Binding( "shift+right", "next", "Next" ),
    ]

    def compose( self ) -> ComposeResult:
        yield Header()
        with Vertical():
            with TabbedContent():
                for n in range( 6 ):
                    yield SelfFocusPane( f"Tab {n}")
        yield Footer()

    def on_mount(self) -> None:
        self.query_one(Tabs).focus()

    def action_previous(self) -> None:
        self.query_one(Tabs).action_previous_tab()

    def action_next(self) -> None:
        self.query_one(Tabs).action_next_tab()

if __name__ == "__main__":
    TabbedContentIssueApp().run()
```

In experimenting, it looks like the `SelfFocusPane.on_show` setting focus to the child of the pane is key here; remove that and I can't recreate the issue.

Changed files:
src/textual/widgets/_tabs.py

diff --git a/src/textual/widgets/_tabs.py b/src/textual/widgets/_tabs.py
index 3191b1e47..309419c0c 100644
--- a/src/textual/widgets/_tabs.py
+++ b/src/textual/widgets/_tabs.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+from time import monotonic
 from typing import TYPE_CHECKING, ClassVar
 
 import rich.repr
@@ -22,6 +23,10 @@ if TYPE_CHECKING:
     from typing_extensions import Self
 
 
+THROTTLE_DELTA = 0.005
+"""Don't call `watch_active` more than once every THROTTLE_DELTA seconds."""
+
+
 class Underline(Widget):
     """The animated underline beneath tabs."""
 
@@ -175,6 +180,9 @@ class Tabs(Widget, can_focus=True):
     | right | Move to the next tab. |
     """
 
+    _last_update: float = -1
+    """Time of last update to throttle updates."""
+
     class TabActivated(Message):
         """Sent when a new tab is activated."""
 
@@ -399,7 +407,16 @@ class Tabs(Widget, can_focus=True):
                 yield Underline()
 
     def watch_active(self, previously_active: str, active: str) -> None:
-        """Handle a change to the active tab."""
+        """Handle a change to the active tab.
+
+        This function is throttled to only run once every THROTTLE_DELTA seconds.
+        """
+
+        now = monotonic()
+        if now < self._last_update + THROTTLE_DELTA:
+            return
+        self._last_update = now
+
         if active:
             active_tab = self.query_one(f"#tabs-list > #{active}", Tab)
             self.query("#tabs-list > Tab.-active").remove_class("-active")
@@ -420,7 +437,7 @@ class Tabs(Widget, can_focus=True):
         """
         underline = self.query_one(Underline)
         try:
-            active_tab = self.query_one(f"#tabs-list > Tab.-active")
+            active_tab = self.query_one("#tabs-list > Tab.-active")
         except NoMatches:
             underline.highlight_start = 0
             underline.highlight_end = 0
@@ -504,5 +521,6 @@ class Tabs(Widget, can_focus=True):
             return
         tab_count = len(tabs)
         new_tab_index = (tabs.index(active_tab) + direction) % tab_count
+        print(tabs[new_tab_index].id or "")
         self.active = tabs[new_tab_index].id or ""
         self._scroll_active_tab()