mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #2581 from Textualize/auto-focus-improv
AUTO_FOCUS targets first focusable widget.
This commit is contained in:
@@ -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
|
- 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
|
- `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
|
- 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 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
|
- Fix issue with scrolling and docks https://github.com/Textualize/textual/issues/2525
|
||||||
|
|
||||||
|
|||||||
@@ -2140,6 +2140,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
screen = Screen(id="_default")
|
screen = Screen(id="_default")
|
||||||
self._register(self, screen)
|
self._register(self, screen)
|
||||||
self._screen_stack.append(screen)
|
self._screen_stack.append(screen)
|
||||||
|
screen.post_message(events.ScreenResume())
|
||||||
await super().on_event(event)
|
await super().on_event(event)
|
||||||
|
|
||||||
elif isinstance(event, events.InputEvent) and not event.is_forwarded:
|
elif isinstance(event, events.InputEvent) and not event.is_forwarded:
|
||||||
|
|||||||
@@ -668,15 +668,13 @@ class Screen(Generic[ScreenResultType], Widget):
|
|||||||
"""Screen has resumed."""
|
"""Screen has resumed."""
|
||||||
self.stack_updates += 1
|
self.stack_updates += 1
|
||||||
size = self.app.size
|
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_layout(size, full=True)
|
||||||
self.refresh()
|
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:
|
def _on_screen_suspend(self) -> None:
|
||||||
"""Screen has suspended."""
|
"""Screen has suspended."""
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,4 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -78,8 +77,7 @@ def test_switches(snap_compare):
|
|||||||
|
|
||||||
def test_input_and_focus(snap_compare):
|
def test_input_and_focus(snap_compare):
|
||||||
press = [
|
press = [
|
||||||
"tab",
|
*"Darren", # Write "Darren"
|
||||||
*"Darren", # Focus first input, write "Darren"
|
|
||||||
"tab",
|
"tab",
|
||||||
*"Burns", # Focus second input, write "Burns"
|
*"Burns", # Focus second input, write "Burns"
|
||||||
]
|
]
|
||||||
@@ -88,7 +86,7 @@ def test_input_and_focus(snap_compare):
|
|||||||
|
|
||||||
def test_buttons_render(snap_compare):
|
def test_buttons_render(snap_compare):
|
||||||
# Testing button rendering. We press tab to focus the first button too.
|
# 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):
|
def test_placeholder_render(snap_compare):
|
||||||
@@ -189,7 +187,7 @@ def test_content_switcher_example_initial(snap_compare):
|
|||||||
def test_content_switcher_example_switch(snap_compare):
|
def test_content_switcher_example_switch(snap_compare):
|
||||||
assert snap_compare(
|
assert snap_compare(
|
||||||
WIDGET_EXAMPLES_DIR / "content_switcher.py",
|
WIDGET_EXAMPLES_DIR / "content_switcher.py",
|
||||||
press=["tab", "tab", "enter", "wait:500"],
|
press=["tab", "enter", "wait:500"],
|
||||||
terminal_size=(50, 50),
|
terminal_size=(50, 50),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -315,7 +313,7 @@ def test_programmatic_scrollbar_gutter_change(snap_compare):
|
|||||||
|
|
||||||
|
|
||||||
def test_borders_preview(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):
|
def test_colors_preview(snap_compare):
|
||||||
@@ -379,9 +377,7 @@ def test_disabled_widgets(snap_compare):
|
|||||||
|
|
||||||
|
|
||||||
def test_focus_component_class(snap_compare):
|
def test_focus_component_class(snap_compare):
|
||||||
assert snap_compare(
|
assert snap_compare(SNAPSHOT_APPS_DIR / "focus_component_class.py", press=["tab"])
|
||||||
SNAPSHOT_APPS_DIR / "focus_component_class.py", press=["tab", "tab"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_line_api_scrollbars(snap_compare):
|
def test_line_api_scrollbars(snap_compare):
|
||||||
@@ -442,7 +438,7 @@ def test_modal_dialog_bindings_input(snap_compare):
|
|||||||
# Check https://github.com/Textualize/textual/issues/2194
|
# Check https://github.com/Textualize/textual/issues/2194
|
||||||
assert snap_compare(
|
assert snap_compare(
|
||||||
SNAPSHOT_APPS_DIR / "modal_screen_bindings.py",
|
SNAPSHOT_APPS_DIR / "modal_screen_bindings.py",
|
||||||
press=["enter", "tab", "h", "!", "left", "i", "tab"],
|
press=["enter", "h", "!", "left", "i", "tab"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -518,6 +514,5 @@ def test_select_rebuild(snap_compare):
|
|||||||
# https://github.com/Textualize/textual/issues/2557
|
# https://github.com/Textualize/textual/issues/2557
|
||||||
assert snap_compare(
|
assert snap_compare(
|
||||||
SNAPSHOT_APPS_DIR / "select_rebuild.py",
|
SNAPSHOT_APPS_DIR / "select_rebuild.py",
|
||||||
press=["tab", "space", "escape", "tab", "enter", "tab", "space"]
|
press=["space", "escape", "tab", "enter", "tab", "space"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.widgets import Button
|
from textual.widgets import Button, Input
|
||||||
|
|
||||||
|
|
||||||
def test_batch_update():
|
def test_batch_update():
|
||||||
@@ -20,6 +20,7 @@ def test_batch_update():
|
|||||||
|
|
||||||
class MyApp(App):
|
class MyApp(App):
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Input()
|
||||||
yield Button("Click me!")
|
yield Button("Click me!")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ async def test_on_button_pressed() -> None:
|
|||||||
|
|
||||||
app = ButtonApp()
|
app = ButtonApp()
|
||||||
async with app.run_test() as pilot:
|
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()
|
await pilot.pause()
|
||||||
|
|
||||||
assert pressed == [
|
assert pressed == [
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ async def test_empty_paste():
|
|||||||
|
|
||||||
app = PasteApp()
|
app = PasteApp()
|
||||||
async with app.run_test() as pilot:
|
async with app.run_test() as pilot:
|
||||||
|
app.set_focus(None)
|
||||||
await pilot.press("p")
|
await pilot.press("p")
|
||||||
assert app.query_one(MyInput).value == ""
|
assert app.query_one(MyInput).value == ""
|
||||||
assert len(paste_events) == 1
|
assert len(paste_events) == 1
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import pytest
|
|||||||
|
|
||||||
from textual.app import App, ScreenStackError
|
from textual.app import App, ScreenStackError
|
||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
from textual.widgets import Button, Input
|
from textual.widgets import Button, Input, Label
|
||||||
|
|
||||||
skip_py310 = pytest.mark.skipif(
|
skip_py310 = pytest.mark.skipif(
|
||||||
sys.version_info.minor == 10 and sys.version_info.major == 3,
|
sys.version_info.minor == 10 and sys.version_info.major == 3,
|
||||||
@@ -155,8 +155,7 @@ async def test_screens():
|
|||||||
|
|
||||||
async def test_auto_focus():
|
async def test_auto_focus():
|
||||||
class MyScreen(Screen[None]):
|
class MyScreen(Screen[None]):
|
||||||
def compose(self) -> None:
|
def compose(self):
|
||||||
print("composing")
|
|
||||||
yield Button()
|
yield Button()
|
||||||
yield Input(id="one")
|
yield Input(id="one")
|
||||||
yield Input(id="two")
|
yield Input(id="two")
|
||||||
@@ -194,6 +193,22 @@ async def test_auto_focus():
|
|||||||
assert app.focused.id == "two"
|
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():
|
async def test_dismiss_non_top_screen():
|
||||||
class MyApp(App[None]):
|
class MyApp(App[None]):
|
||||||
async def key_p(self) -> None:
|
async def key_p(self) -> None:
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ async def test_radio_sets_initial_state():
|
|||||||
async def test_click_sets_focus():
|
async def test_click_sets_focus():
|
||||||
"""Clicking within a radio set should set focus."""
|
"""Clicking within a radio set should set focus."""
|
||||||
async with RadioSetApp().run_test() as pilot:
|
async with RadioSetApp().run_test() as pilot:
|
||||||
|
pilot.app.set_focus(None)
|
||||||
assert pilot.app.screen.focused is None
|
assert pilot.app.screen.focused is None
|
||||||
await pilot.click("#clickme")
|
await pilot.click("#clickme")
|
||||||
assert pilot.app.screen.focused == pilot.app.query_one("#from_buttons")
|
assert pilot.app.screen.focused == pilot.app.query_one("#from_buttons")
|
||||||
@@ -72,8 +73,6 @@ async def test_radioset_same_button_mash():
|
|||||||
async def test_radioset_inner_navigation():
|
async def test_radioset_inner_navigation():
|
||||||
"""Using the cursor keys should navigate between buttons in a set."""
|
"""Using the cursor keys should navigate between buttons in a set."""
|
||||||
async with RadioSetApp().run_test() as pilot:
|
async with RadioSetApp().run_test() as pilot:
|
||||||
assert pilot.app.screen.focused is None
|
|
||||||
await pilot.press("tab")
|
|
||||||
for key, landing in (
|
for key, landing in (
|
||||||
("down", 1),
|
("down", 1),
|
||||||
("up", 0),
|
("up", 0),
|
||||||
@@ -88,8 +87,6 @@ async def test_radioset_inner_navigation():
|
|||||||
== pilot.app.query_one("#from_buttons").children[landing]
|
== pilot.app.query_one("#from_buttons").children[landing]
|
||||||
)
|
)
|
||||||
async with RadioSetApp().run_test() as pilot:
|
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")
|
assert pilot.app.screen.focused is pilot.app.screen.query_one("#from_buttons")
|
||||||
await pilot.press("tab")
|
await pilot.press("tab")
|
||||||
assert pilot.app.screen.focused is pilot.app.screen.query_one("#from_strings")
|
assert pilot.app.screen.focused is pilot.app.screen.query_one("#from_strings")
|
||||||
@@ -101,8 +98,6 @@ async def test_radioset_inner_navigation():
|
|||||||
async def test_radioset_breakout_navigation():
|
async def test_radioset_breakout_navigation():
|
||||||
"""Shift/Tabbing while in a radioset should move to the previous/next focsuable after the set itself."""
|
"""Shift/Tabbing while in a radioset should move to the previous/next focsuable after the set itself."""
|
||||||
async with RadioSetApp().run_test() as pilot:
|
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")
|
assert pilot.app.screen.focused is pilot.app.query_one("#from_buttons")
|
||||||
await pilot.press("tab")
|
await pilot.press("tab")
|
||||||
assert pilot.app.screen.focused is pilot.app.query_one("#from_strings")
|
assert pilot.app.screen.focused is pilot.app.query_one("#from_strings")
|
||||||
|
|||||||
Reference in New Issue
Block a user