diff --git a/CHANGELOG.md b/CHANGELOG.md index 5991f818d..216339e66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed `TreeNode.toggle` and `TreeNode.toggle_all` not posting a `Tree.NodeExpanded` or `Tree.NodeCollapsed` message https://github.com/Textualize/textual/issues/2535 - `footer--description` component class was being ignored https://github.com/Textualize/textual/issues/2544 - Pasting empty selection in `Input` would raise an exception https://github.com/Textualize/textual/issues/2563 +- `Screen.AUTO_FOCUS` now focuses the first _focusable_ widget that matches the selector https://github.com/Textualize/textual/issues/2578 ### Added diff --git a/src/textual/screen.py b/src/textual/screen.py index af0b006be..9f065dfbf 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -668,11 +668,14 @@ class Screen(Generic[ScreenResultType], Widget): 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() + focus_candidates = self.query(self.AUTO_FOCUS) except NoMatches: pass else: - self.set_focus(to_focus) + for widget in focus_candidates: + if widget.focusable: + self.set_focus(widget) + break self._refresh_layout(size, full=True) self.refresh() diff --git a/tests/test_screens.py b/tests/test_screens.py index 2e3dbfcbe..7ddc8b20e 100644 --- a/tests/test_screens.py +++ b/tests/test_screens.py @@ -6,7 +6,7 @@ import pytest from textual.app import App, ScreenStackError from textual.screen import Screen -from textual.widgets import Button, Input +from textual.widgets import Button, Input, Label skip_py310 = pytest.mark.skipif( sys.version_info.minor == 10 and sys.version_info.major == 3, @@ -155,8 +155,7 @@ async def test_screens(): async def test_auto_focus(): class MyScreen(Screen[None]): - def compose(self) -> None: - print("composing") + def compose(self): yield Button() yield Input(id="one") yield Input(id="two") @@ -192,3 +191,19 @@ async def test_auto_focus(): assert app.focused is None app.pop_screen() assert app.focused.id == "two" + + +async def test_auto_focus_skips_non_focusable_widgets(): + class MyScreen(Screen[None]): + def compose(self): + yield Label() + yield Button() + + class MyApp(App[None]): + def on_mount(self): + self.push_screen(MyScreen()) + + app = MyApp() + async with app.run_test(): + assert app.focused is not None + assert isinstance(app.focused, Button)