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 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ 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
- `Screen.AUTO_FOCUS` now works on the default screen on startup https://github.com/Textualize/textual/pull/2581
- Fix for setting dark in App `__init__` https://github.com/Textualize/textual/issues/2583
- Fix issue with scrolling and docks https://github.com/Textualize/textual/issues/2525

Expand Down
1 change: 1 addition & 0 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2140,6 +2140,7 @@ async def on_event(self, event: events.Event) -> None:
screen = Screen(id="_default")
self._register(self, screen)
self._screen_stack.append(screen)
screen.post_message(events.ScreenResume())
await super().on_event(event)

elif isinstance(event, events.InputEvent) and not event.is_forwarded:
Expand Down
12 changes: 5 additions & 7 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,15 +668,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()
if self.AUTO_FOCUS is not None and self.focused is None:
for widget in self.query(self.AUTO_FOCUS):
if widget.focusable:
self.set_focus(widget)
break

def _on_screen_suspend(self) -> None:
"""Screen has suspended."""
Expand Down
1,808 changes: 907 additions & 901 deletions tests/snapshot_tests/__snapshots__/test_snapshots.ambr

Large diffs are not rendered by default.

19 changes: 7 additions & 12 deletions tests/snapshot_tests/test_snapshots.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from pathlib import Path
import sys

import pytest

Expand Down Expand Up @@ -78,8 +77,7 @@ def test_switches(snap_compare):

def test_input_and_focus(snap_compare):
press = [
"tab",
*"Darren", # Focus first input, write "Darren"
*"Darren", # Write "Darren"
"tab",
*"Burns", # Focus second input, write "Burns"
]
Expand All @@ -88,7 +86,7 @@ def test_input_and_focus(snap_compare):

def test_buttons_render(snap_compare):
# Testing button rendering. We press tab to focus the first button too.
assert snap_compare(WIDGET_EXAMPLES_DIR / "button.py", press=["tab", "tab"])
assert snap_compare(WIDGET_EXAMPLES_DIR / "button.py", press=["tab"])


def test_placeholder_render(snap_compare):
Expand Down Expand Up @@ -189,7 +187,7 @@ def test_content_switcher_example_initial(snap_compare):
def test_content_switcher_example_switch(snap_compare):
assert snap_compare(
WIDGET_EXAMPLES_DIR / "content_switcher.py",
press=["tab", "tab", "enter", "wait:500"],
press=["tab", "enter", "wait:500"],
terminal_size=(50, 50),
)

Expand Down Expand Up @@ -315,7 +313,7 @@ def test_programmatic_scrollbar_gutter_change(snap_compare):


def test_borders_preview(snap_compare):
assert snap_compare(CLI_PREVIEWS_DIR / "borders.py", press=["tab", "enter"])
assert snap_compare(CLI_PREVIEWS_DIR / "borders.py", press=["enter"])


def test_colors_preview(snap_compare):
Expand Down Expand Up @@ -379,9 +377,7 @@ def test_disabled_widgets(snap_compare):


def test_focus_component_class(snap_compare):
assert snap_compare(
SNAPSHOT_APPS_DIR / "focus_component_class.py", press=["tab", "tab"]
)
assert snap_compare(SNAPSHOT_APPS_DIR / "focus_component_class.py", press=["tab"])


def test_line_api_scrollbars(snap_compare):
Expand Down Expand Up @@ -442,7 +438,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 Expand Up @@ -518,6 +514,5 @@ def test_select_rebuild(snap_compare):
# https://github.com/Textualize/textual/issues/2557
assert snap_compare(
SNAPSHOT_APPS_DIR / "select_rebuild.py",
press=["tab", "space", "escape", "tab", "enter", "tab", "space"]
press=["space", "escape", "tab", "enter", "tab", "space"],
)

3 changes: 2 additions & 1 deletion tests/test_app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from textual.app import App, ComposeResult
from textual.widgets import Button
from textual.widgets import Button, Input


def test_batch_update():
Expand All @@ -20,6 +20,7 @@ def test_batch_update():

class MyApp(App):
def compose(self) -> ComposeResult:
yield Input()
yield Button("Click me!")


Expand Down
2 changes: 1 addition & 1 deletion tests/test_on.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def on_button_pressed(self):

app = ButtonApp()
async with app.run_test() as pilot:
await pilot.press("tab", "enter", "tab", "enter", "tab", "enter")
await pilot.press("enter", "tab", "enter", "tab", "enter")
await pilot.pause()

assert pressed == [
Expand Down
1 change: 1 addition & 0 deletions tests/test_paste.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def key_p(self):

app = PasteApp()
async with app.run_test() as pilot:
app.set_focus(None)
await pilot.press("p")
assert app.query_one(MyInput).value == ""
assert len(paste_events) == 1
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
7 changes: 1 addition & 6 deletions tests/toggles/test_radioset.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ async def test_radio_sets_initial_state():
async def test_click_sets_focus():
"""Clicking within a radio set should set focus."""
async with RadioSetApp().run_test() as pilot:
pilot.app.set_focus(None)
assert pilot.app.screen.focused is None
await pilot.click("#clickme")
assert pilot.app.screen.focused == pilot.app.query_one("#from_buttons")
Expand Down Expand Up @@ -72,8 +73,6 @@ async def test_radioset_same_button_mash():
async def test_radioset_inner_navigation():
"""Using the cursor keys should navigate between buttons in a set."""
async with RadioSetApp().run_test() as pilot:
assert pilot.app.screen.focused is None
await pilot.press("tab")
for key, landing in (
("down", 1),
("up", 0),
Expand All @@ -88,8 +87,6 @@ async def test_radioset_inner_navigation():
== pilot.app.query_one("#from_buttons").children[landing]
)
async with RadioSetApp().run_test() as pilot:
assert pilot.app.screen.focused is None
await pilot.press("tab")
assert pilot.app.screen.focused is pilot.app.screen.query_one("#from_buttons")
await pilot.press("tab")
assert pilot.app.screen.focused is pilot.app.screen.query_one("#from_strings")
Expand All @@ -101,8 +98,6 @@ async def test_radioset_inner_navigation():
async def test_radioset_breakout_navigation():
"""Shift/Tabbing while in a radioset should move to the previous/next focsuable after the set itself."""
async with RadioSetApp().run_test() as pilot:
assert pilot.app.screen.focused is None
await pilot.press("tab")
assert pilot.app.screen.focused is pilot.app.query_one("#from_buttons")
await pilot.press("tab")
assert pilot.app.screen.focused is pilot.app.query_one("#from_strings")
Expand Down