Skip to content

Commit

Permalink
Merge pull request #2581 from Textualize/auto-focus-improv
Browse files Browse the repository at this point in the history
AUTO_FOCUS targets first focusable widget.
  • Loading branch information
rodrigogiraoserrao committed May 17, 2023
2 parents f820598 + 38f9500 commit 179a850
Show file tree
Hide file tree
Showing 10 changed files with 945 additions and 931 deletions.
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

0 comments on commit 179a850

Please sign in to comment.