Repository: Textualize/textual
Pull request: Add `auto_focus` to screens
This will close #2457.

I've added a docstring but I haven't added documentation elsewhere. Should I?

Problem statement:
Add option to auto focus the first thing on a screen.
I'm thinking that screen could grow a `auto_focus` parameter which takes a selector. It would then focus the first matching node, if there is one.

This would need to be done when the screen is first made active, and when it is resumed. i.e. if a screen is popped off the stack, the new one should focus.

`auto_focus` should be `"*"` by default to focus the first focusable thing. `auto_focus` may be set to `None` to disable auto focus.

Hints from issue discussion:
For the case where there's a stack with 2+ screens, when we pop a screen and we go back to a previous screen, I think we should preserve the widget that was already focused previously.

Commit message: Add auto_focus attribute to screens.

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa2923fac6..d5f95b549c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](http://keepachangelog.com/)
 and this project adheres to [Semantic Versioning](http://semver.org/).
 
+
+## Unreleased
+
+### Added
+
+- Attribute `auto_focus` to screens https://github.com/Textualize/textual/issues/2457
+
 ## [0.24.1] - 2023-05-08
 
 ### Fixed
diff --git a/src/textual/screen.py b/src/textual/screen.py
index 34db473efc..8bc9c0532e 100644
--- a/src/textual/screen.py
+++ b/src/textual/screen.py
@@ -30,7 +30,7 @@
 from .binding import Binding
 from .css.match import match
 from .css.parse import parse_selectors
-from .css.query import QueryType
+from .css.query import NoMatches, QueryType
 from .dom import DOMNode
 from .geometry import Offset, Region, Size
 from .reactive import Reactive
@@ -101,6 +101,12 @@ class Screen(Generic[ScreenResultType], Widget):
     }
     """
 
+    auto_focus: str | None = "*"
+    """A selector to determine what to focus automatically when the screen is activated.
+
+    The widget focused is the first that matches the given [CSS selector](/guide/queries/#query-selectors).
+    Set to `None` to disable auto focus.
+    """
     focused: Reactive[Widget | None] = Reactive(None)
     """The focused [widget][textual.widget.Widget] or `None` for no focus."""
     stack_updates: Reactive[int] = Reactive(0, repaint=False)
@@ -659,6 +665,13 @@ def _on_screen_resume(self) -> None:
         """Screen has resumed."""
         self.stack_updates += 1
         size = self.app.size
+        if self.auto_focus is not None and self.focused is None:
+            try:
+                to_focus = self.query(self.auto_focus).first()
+            except NoMatches:
+                pass
+            else:
+                self.set_focus(to_focus)
         self._refresh_layout(size, full=True)
         self.refresh()
 
diff --git a/tests/test_screens.py b/tests/test_screens.py
index 6825c101c5..7e20348a9a 100644
--- a/tests/test_screens.py
+++ b/tests/test_screens.py
@@ -6,6 +6,7 @@
 
 from textual.app import App, ScreenStackError
 from textual.screen import Screen
+from textual.widgets import Button, Input
 
 skip_py310 = pytest.mark.skipif(
     sys.version_info.minor == 10 and sys.version_info.major == 3,
@@ -150,3 +151,44 @@ async def test_screens():
     screen2.remove()
     screen3.remove()
     await app._shutdown()
+
+
+async def test_auto_focus():
+    class MyScreen(Screen[None]):
+        def compose(self) -> None:
+            print("composing")
+            yield Button()
+            yield Input(id="one")
+            yield Input(id="two")
+
+    class MyApp(App[None]):
+        pass
+
+    app = MyApp()
+    async with app.run_test():
+        await app.push_screen(MyScreen())
+        assert isinstance(app.focused, Button)
+        app.pop_screen()
+
+        MyScreen.auto_focus = None
+        await app.push_screen(MyScreen())
+        assert app.focused is None
+        app.pop_screen()
+
+        MyScreen.auto_focus = "Input"
+        await app.push_screen(MyScreen())
+        assert isinstance(app.focused, Input)
+        assert app.focused.id == "one"
+        app.pop_screen()
+
+        MyScreen.auto_focus = "#two"
+        await app.push_screen(MyScreen())
+        assert isinstance(app.focused, Input)
+        assert app.focused.id == "two"
+
+        # If we push and pop another screen, focus should be preserved for #two.
+        MyScreen.auto_focus = None
+        await app.push_screen(MyScreen())
+        assert app.focused is None
+        app.pop_screen()
+        assert app.focused.id == "two"


Code review comments:
diff_hunk: @@ -101,6 +101,12 @@ class Screen(Generic[ScreenResultType], Widget):
     }
     """
 
+    auto_focus: str | None = "*"
path: src/textual/screen.py
text: @user1:
Let's make this an instance var, which should be the default unless there is good rationale for it being a classvar.

@author:
Mostly because it's a pain to set it as an instance attribute-only for people creating their own screens:

```py
class FormScreen(Screen):
    auto_focus = "Input"

    ...
```

vs 

```py
class FormScreen(Screen):
    def __init__(
        self,
        name: str | None = None,
        id: str | None = None,
        classes: str | None = None,
    ) -> None:
        self.auto_focus = "Input"
        super().__init__(name=name, id=id, classes=classes)

    ...
```

@user1:
If we want it to be a classvar, suggest we make it a constant, i.e. `AUTO_FOCUS`. 


@author:
Classvar it is.