Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 11 additions & 1 deletion src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
67 changes: 66 additions & 1 deletion tests/test_disabled.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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