diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d887f32b8..e9c9c20041 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed exceptions in Pilot tests being silently ignored https://github.com/Textualize/textual/pull/2754 - Fixed issue where internal data of `OptionList` could be invalid for short window after `clear_options` https://github.com/Textualize/textual/pull/2754 - Fixed `Tooltip` causing a `query_one` on a lone `Static` to fail https://github.com/Textualize/textual/issues/2723 +- Nested widgets wouldn't lose focus when parent is disabled https://github.com/Textualize/textual/issues/2772 ### Changed diff --git a/src/textual/widget.py b/src/textual/widget.py index 99e4b78eb4..3306da7ddc 100644 --- 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( diff --git a/tests/test_disabled.py b/tests/test_disabled.py index bc0692ad05..f5edb492c2 100644 --- 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