diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3085106dd..f43453c25 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Removed
- `Placeholder.reset_color_cycle`
+- Removed `Widget.reset_focus` (now called `Widget.blur`) https://github.com/Textualize/textual/issues/2642
## [0.26.0] - 2023-05-20
diff --git a/docs/guide/screens.md b/docs/guide/screens.md
index 0119ef655..25915513c 100644
--- a/docs/guide/screens.md
+++ b/docs/guide/screens.md
@@ -225,8 +225,8 @@ The main screen is darkened to indicate to the user that it is not active, and o
It is a common requirement for screens to be able to return data.
For instance, you may want a screen to show a dialog and have the result of that dialog processed *after* the screen has been popped.
-To return data from a screen, call [`dismiss()`][textual.screen.dismiss] on the screen with the data you wish to return.
-This will pop the screen and invoke a callback set when the screen was pushed (with [`push_screen`][textual.app.push_screen]).
+To return data from a screen, call [`dismiss()`][textual.screen.Screen.dismiss] on the screen with the data you wish to return.
+This will pop the screen and invoke a callback set when the screen was pushed (with [`push_screen`][textual.app.App.push_screen]).
Let's modify the previous example to use `dismiss` rather than an explicit `pop_screen`.
diff --git a/src/textual/app.py b/src/textual/app.py
index aca3be101..fe30f2e1b 100644
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -2075,7 +2075,7 @@ class App(Generic[ReturnType], DOMNode):
Args:
widget: A Widget to unregister
"""
- widget.reset_focus()
+ widget.blur()
if isinstance(widget._parent, Widget):
widget._parent._nodes._remove(widget)
widget._detach()
diff --git a/src/textual/dom.py b/src/textual/dom.py
index c6efd1c4f..12e8a9537 100644
--- a/src/textual/dom.py
+++ b/src/textual/dom.py
@@ -1123,7 +1123,8 @@ class DOMNode(MessagePump):
"""Replace all classes.
Args:
- A string contain space separated classes, or an iterable of class names.
+ classes: A string containing space separated classes, or an
+ iterable of class names.
Returns:
Self.
diff --git a/src/textual/screen.py b/src/textual/screen.py
index 1d04c0602..29c6ab95b 100644
--- a/src/textual/screen.py
+++ b/src/textual/screen.py
@@ -386,6 +386,7 @@ class Screen(Generic[ScreenResultType], Widget):
focusable_widgets = self.focus_chain
if not focusable_widgets:
# If there's nothing to focus... give up now.
+ self.set_focus(None)
return
try:
@@ -778,7 +779,7 @@ class Screen(Generic[ScreenResultType], Widget):
def dismiss(self, result: ScreenResultType | Type[_NoResult] = _NoResult) -> None:
"""Dismiss the screen, optionally with a result.
- If `result` is provided and a callback was set when the screen was [pushed][textual.app.push_screen], then
+ If `result` is provided and a callback was set when the screen was [pushed][textual.app.App.push_screen], then
the callback will be invoked with `result`.
Args:
diff --git a/src/textual/widget.py b/src/textual/widget.py
index d1e7bc64f..a67c3fe27 100644
--- a/src/textual/widget.py
+++ b/src/textual/widget.py
@@ -2712,6 +2712,7 @@ class Widget(DOMNode):
def watch_disabled(self) -> None:
"""Update the styles of the widget and its children when disabled is toggled."""
+ self.blur()
self._update_styles()
def _size_updated(
@@ -3017,8 +3018,10 @@ class Widget(DOMNode):
self.app.call_later(set_focus, self)
return self
- def reset_focus(self) -> Self:
- """Reset the focus (move it to the next available widget).
+ def blur(self) -> Self:
+ """Blur (un-focus) the widget.
+
+ Focus will be moved to the next available widget in the focus chain..
Returns:
The `Widget` instance.
@@ -3172,7 +3175,7 @@ class Widget(DOMNode):
def _on_hide(self, event: events.Hide) -> None:
if self.has_focus:
- self.reset_focus()
+ self.blur()
def _on_scroll_to_region(self, message: messages.ScrollToRegion) -> None:
self.scroll_to_region(message.region, animate=True)
diff --git a/src/textual/widgets/_tabs.py b/src/textual/widgets/_tabs.py
index 78c7a6869..c59cbca16 100644
--- a/src/textual/widgets/_tabs.py
+++ b/src/textual/widgets/_tabs.py
@@ -183,11 +183,6 @@ class Tabs(Widget, can_focus=True):
ALLOW_SELECTOR_MATCH = {"tab"}
"""Additional message attributes that can be used with the [`on` decorator][textual.on]."""
- tabs: Tabs
- """The tabs widget containing the tab."""
- tab: Tab
- """The tab that was activated."""
-
def __init__(self, tabs: Tabs, tab: Tab) -> None:
"""Initialize event.
@@ -195,8 +190,10 @@ class Tabs(Widget, can_focus=True):
tabs: The Tabs widget.
tab: The tab that was activated.
"""
- self.tabs = tabs
- self.tab = tab
+ self.tabs: Tabs = tabs
+ """The tabs widget containing the tab."""
+ self.tab: Tab = tab
+ """The tab that was activated."""
super().__init__()
@property
@@ -215,16 +212,14 @@ class Tabs(Widget, can_focus=True):
class Cleared(Message):
"""Sent when there are no active tabs."""
- tabs: Tabs
- """The tabs widget which was cleared."""
-
def __init__(self, tabs: Tabs) -> None:
"""Initialize the event.
Args:
tabs: The tabs widget.
"""
- self.tabs = tabs
+ self.tabs: Tabs = tabs
+ """The tabs widget which was cleared."""
super().__init__()
@property
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
index e39be06d6..7013a574a 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -709,6 +709,164 @@
'''
# ---
+# name: test_blur_on_disabled
+ '''
+
+
+ '''
+# ---
# name: test_border_alpha
'''
diff --git a/tests/snapshot_tests/snapshot_apps/blur_on_disabled.py b/tests/snapshot_tests/snapshot_apps/blur_on_disabled.py
new file mode 100644
index 000000000..3bfa0e122
--- /dev/null
+++ b/tests/snapshot_tests/snapshot_apps/blur_on_disabled.py
@@ -0,0 +1,20 @@
+from textual.app import App, ComposeResult
+from textual.widgets import Input
+
+
+class BlurApp(App):
+ BINDINGS = [("f3", "disable")]
+
+ def compose(self) -> ComposeResult:
+ yield Input()
+
+ def on_ready(self) -> None:
+ self.query_one(Input).focus()
+
+ def action_disable(self) -> None:
+ self.query_one(Input).disabled = True
+
+
+if __name__ == "__main__":
+ app = BlurApp()
+ app.run()
diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py
index 874f096d9..fd22c0ca3 100644
--- a/tests/snapshot_tests/test_snapshots.py
+++ b/tests/snapshot_tests/test_snapshots.py
@@ -514,3 +514,11 @@ def test_select_rebuild(snap_compare):
SNAPSHOT_APPS_DIR / "select_rebuild.py",
press=["space", "escape", "tab", "enter", "tab", "space"],
)
+
+
+def test_blur_on_disabled(snap_compare):
+ # https://github.com/Textualize/textual/issues/2641
+ assert snap_compare(
+ SNAPSHOT_APPS_DIR / "blur_on_disabled.py",
+ press=[*"foo", "f3", *"this should not appear"],
+ )