Repository: Textualize/textual

Issue:
Shouldn't be able to type in to a disabled input
If a widget is *enabled* and has focus, then subsequently *disabled*, you are still able to type.

When an input is disabled, it should ignore all interactions.


Discussion:
~~I cannot reproduce this, @willmcgugan.~~

[Repro app](https://github.com/Textualize/textual/issues/2772#issuecomment-1589316642)
Can't reproduce with this app:

```python
class DisabledInput(App):
    BINDINGS = [
        Binding("ctrl+n", "disable_input", "Disable Input"),
    ]

    def compose(self) -> ComposeResult:
        yield Input("Disabled")

    def action_disable_input(self) -> None:
        self.query_one(Input).disabled = True


app = DisabledInput()
if __name__ == '__main__':
    app.run()

```
This does it:

```python
from textual.app        import App, ComposeResult
from textual.containers import Vertical
from textual.widgets    import Input

class DisableFocus( App[ None ] ):

    BINDINGS = [
        ( "ctrl+z", "disable" ),
    ]

    def compose( self ) -> ComposeResult:
        with Vertical():
            yield Input()

    def action_disable( self ) -> None:
        self.query_one( Vertical ).disabled = not self.query_one( Vertical ).disabled

if __name__ == "__main__":
    DisableFocus().run()
```

Resolving patch:
diff --git a/src/textual/widget.py b/src/textual/widget.py
--- a/src/textual/widget.py
+++ b/src/textual/widget.py
@@ -2743,7 +2743,17 @@ def watch_has_focus(self, value: bool) -> None:
 
     def watch_disabled(self) -> None:
         """Update the styles of the widget and its children when disabled is toggled."""
-        self.blur()
+        from .app import ScreenStackError
+
+        try:
+            if (
+                self.disabled
+                and self.app.focused is not None
+                and self in self.app.focused.ancestors_with_self
+            ):
+                self.app.focused.blur()
+        except ScreenStackError:
+            pass
         self._update_styles()
 
     def _size_updated(


Test patch:
diff --git a/tests/test_disabled.py b/tests/test_disabled.py
--- a/tests/test_disabled.py
+++ b/tests/test_disabled.py
@@ -1,15 +1,24 @@
 """Test Widget.disabled."""
 
+import pytest
+
 from textual.app import App, ComposeResult
-from textual.containers import VerticalScroll
+from textual.containers import Vertical, VerticalScroll
 from textual.widgets import (
     Button,
+    Checkbox,
     DataTable,
     DirectoryTree,
     Input,
+    Label,
+    ListItem,
     ListView,
     Markdown,
     MarkdownViewer,
+    OptionList,
+    RadioButton,
+    RadioSet,
+    Select,
     Switch,
     TextLog,
     Tree,
@@ -82,3 +91,59 @@ async def test_disable_via_container() -> None:
             node.has_pseudo_class("disabled") and not node.has_pseudo_class("enabled")
             for node in pilot.app.screen.query("#test-container > *")
         )
+
+
+class ChildrenNoFocusDisabledContainer(App[None]):
+    """App for regression test for https://github.com/Textualize/textual/issues/2772."""
+
+    def compose(self) -> ComposeResult:
+        with Vertical():
+            with Vertical():
+                yield Button()
+                yield Checkbox()
+                yield DataTable()
+                yield DirectoryTree(".")
+                yield Input()
+                with ListView():
+                    yield ListItem(Label("one"))
+                    yield ListItem(Label("two"))
+                    yield ListItem(Label("three"))
+                yield OptionList("one", "two", "three")
+                with RadioSet():
+                    yield RadioButton("one")
+                    yield RadioButton("two")
+                    yield RadioButton("three")
+                yield Select([("one", 1), ("two", 2), ("three", 3)])
+                yield Switch()
+
+    def on_mount(self):
+        dt = self.query_one(DataTable)
+        dt.add_columns("one", "two", "three")
+        dt.add_rows([["a", "b", "c"], ["d", "e", "f"], ["g", "h", "i"]])
+
+
+@pytest.mark.parametrize(
+    "widget",
+    [
+        Button,
+        Checkbox,
+        DataTable,
+        DirectoryTree,
+        Input,
+        ListView,
+        OptionList,
+        RadioSet,
+        Select,
+        Switch,
+    ],
+)
+async def test_children_loses_focus_if_container_is_disabled(widget):
+    """Regression test for https://github.com/Textualize/textual/issues/2772."""
+    app = ChildrenNoFocusDisabledContainer()
+    async with app.run_test() as pilot:
+        app.query(widget).first().focus()
+        await pilot.pause()
+        assert isinstance(app.focused, widget)
+        app.query(Vertical).first().disabled = True
+        await pilot.pause()
+        assert app.focused is None