Skip to content

Commit

Permalink
Merge pull request #2527 from Textualize/auto-focus
Browse files Browse the repository at this point in the history
Add `auto_focus` to screens
  • Loading branch information
rodrigogiraoserrao authored May 15, 2023
2 parents 4db54ea + 0b6e3b3 commit 83618db
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ 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

### Added

- Class variable `AUTO_FOCUS` to screens https://github.com/Textualize/textual/issues/2457

## [0.24.1] - 2023-05-08

### Fixed
Expand Down
18 changes: 16 additions & 2 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
TYPE_CHECKING,
Awaitable,
Callable,
ClassVar,
Generic,
Iterable,
Iterator,
Expand All @@ -30,7 +31,7 @@
from .binding import Binding
from .css.match import match
from .css.parse import parse_selectors
from .css.query import QueryType
from .css.query import NoMatches, QueryType
from .dom import DOMNode
from .geometry import Offset, Region, Size
from .reactive import Reactive
Expand Down Expand Up @@ -93,14 +94,20 @@ def __call__(self, result: ScreenResultType) -> None:
class Screen(Generic[ScreenResultType], Widget):
"""The base class for screens."""

AUTO_FOCUS: ClassVar[str | None] = "*"
"""A selector to determine what to focus automatically when the screen is activated.
The widget focused is the first that matches the given [CSS selector](/guide/queries/#query-selectors).
Set to `None` to disable auto focus.
"""

DEFAULT_CSS = """
Screen {
layout: vertical;
overflow-y: auto;
background: $surface;
}
"""

focused: Reactive[Widget | None] = Reactive(None)
"""The focused [widget][textual.widget.Widget] or `None` for no focus."""
stack_updates: Reactive[int] = Reactive(0, repaint=False)
Expand Down Expand Up @@ -659,6 +666,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()

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

from textual.app import App, ScreenStackError
from textual.screen import Screen
from textual.widgets import Button, Input

skip_py310 = pytest.mark.skipif(
sys.version_info.minor == 10 and sys.version_info.major == 3,
Expand Down Expand Up @@ -150,3 +151,44 @@ async def test_screens():
screen2.remove()
screen3.remove()
await app._shutdown()


async def test_auto_focus():
class MyScreen(Screen[None]):
def compose(self) -> None:
print("composing")
yield Button()
yield Input(id="one")
yield Input(id="two")

class MyApp(App[None]):
pass

app = MyApp()
async with app.run_test():
await app.push_screen(MyScreen())
assert isinstance(app.focused, Button)
app.pop_screen()

MyScreen.AUTO_FOCUS = None
await app.push_screen(MyScreen())
assert app.focused is None
app.pop_screen()

MyScreen.AUTO_FOCUS = "Input"
await app.push_screen(MyScreen())
assert isinstance(app.focused, Input)
assert app.focused.id == "one"
app.pop_screen()

MyScreen.AUTO_FOCUS = "#two"
await app.push_screen(MyScreen())
assert isinstance(app.focused, Input)
assert app.focused.id == "two"

# If we push and pop another screen, focus should be preserved for #two.
MyScreen.AUTO_FOCUS = None
await app.push_screen(MyScreen())
assert app.focused is None
app.pop_screen()
assert app.focused.id == "two"

0 comments on commit 83618db

Please sign in to comment.