From f9efb88111083effc61ecb3c7ed425f9c31aa957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 13 Jun 2023 16:14:09 +0100 Subject: [PATCH 1/4] Add regression test for #2772. --- tests/test_disabled.py | 67 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) 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 From 9ca0846699f22fcb70e47413affb8e588f0be869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 13 Jun 2023 16:14:24 +0100 Subject: [PATCH 2/4] Remove focus on nested disabled widgets. --- CHANGELOG.md | 1 + src/textual/widget.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) 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..5e77449fd8 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2743,7 +2743,12 @@ 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() + if ( + self.disabled + and self.app.focused is not None + and self.app.focused in self.walk_children() + ): + self.app.focused.blur() self._update_styles() def _size_updated( From a45b156bb3188dff35330ebae82c92ce39a83b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 13 Jun 2023 16:38:52 +0100 Subject: [PATCH 3/4] Optimisation. Related comments: https://github.com/Textualize/textual/pull/2776#discussion_r1228327427 --- src/textual/widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 5e77449fd8..bc2c7743a4 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2746,7 +2746,7 @@ def watch_disabled(self) -> None: if ( self.disabled and self.app.focused is not None - and self.app.focused in self.walk_children() + and self in self.app.focused.ancestors_with_self ): self.app.focused.blur() self._update_styles() From d6dc8d9104e68d9e2a2d369a0fa7c86d35fe68d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 13 Jun 2023 18:15:35 +0100 Subject: [PATCH 4/4] Fix tests. --- src/textual/widget.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index bc2c7743a4..3306da7ddc 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2743,12 +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.""" - if ( - self.disabled - and self.app.focused is not None - and self in self.app.focused.ancestors_with_self - ): - self.app.focused.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(