Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AUTO_FOCUS targets first focusable widget. #2581

Merged
merged 8 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -22,6 +22,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

Expand Down
11 changes: 7 additions & 4 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,15 +666,18 @@ def _on_screen_resume(self) -> None:
"""Screen has resumed."""
self.stack_updates += 1
size = self.app.size
self._refresh_layout(size, full=True)
self.refresh()
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)
rodrigogiraoserrao marked this conversation as resolved.
Show resolved Hide resolved
except NoMatches:
pass
else:
self.set_focus(to_focus)
self._refresh_layout(size, full=True)
self.refresh()
for widget in focus_candidates:
if widget.focusable:
self.set_focus(widget)
break

def _on_screen_suspend(self) -> None:
"""Screen has suspended."""
Expand Down
4 changes: 3 additions & 1 deletion tests/snapshot_tests/test_snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,11 @@ def test_option_list(snap_compare):
assert snap_compare(WIDGET_EXAMPLES_DIR / "option_list_options.py")
assert snap_compare(WIDGET_EXAMPLES_DIR / "option_list_tables.py")


def test_option_list_build(snap_compare):
assert snap_compare(SNAPSHOT_APPS_DIR / "option_list.py")


def test_progress_bar_indeterminate(snap_compare):
assert snap_compare(WIDGET_EXAMPLES_DIR / "progress_bar_isolated_.py", press=["f"])

Expand Down Expand Up @@ -440,7 +442,7 @@ def test_modal_dialog_bindings_input(snap_compare):
# Check https://github.com/Textualize/textual/issues/2194
assert snap_compare(
SNAPSHOT_APPS_DIR / "modal_screen_bindings.py",
press=["enter", "tab", "h", "!", "left", "i", "tab"],
press=["enter", "h", "!", "left", "i", "tab"],
)


Expand Down
21 changes: 18 additions & 3 deletions tests/test_screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

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,
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -194,6 +193,22 @@ class MyApp(App[None]):
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)


async def test_dismiss_non_top_screen():
class MyApp(App[None]):
async def key_p(self) -> None:
Expand Down