mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #2527 from Textualize/auto-focus
Add `auto_focus` to screens
This commit is contained in:
@@ -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
|
- 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
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Class variable `AUTO_FOCUS` to screens https://github.com/Textualize/textual/issues/2457
|
||||||
|
|
||||||
## [0.24.1] - 2023-05-08
|
## [0.24.1] - 2023-05-08
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from typing import (
|
|||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Awaitable,
|
Awaitable,
|
||||||
Callable,
|
Callable,
|
||||||
|
ClassVar,
|
||||||
Generic,
|
Generic,
|
||||||
Iterable,
|
Iterable,
|
||||||
Iterator,
|
Iterator,
|
||||||
@@ -30,7 +31,7 @@ from ._types import CallbackType
|
|||||||
from .binding import Binding
|
from .binding import Binding
|
||||||
from .css.match import match
|
from .css.match import match
|
||||||
from .css.parse import parse_selectors
|
from .css.parse import parse_selectors
|
||||||
from .css.query import QueryType
|
from .css.query import NoMatches, QueryType
|
||||||
from .dom import DOMNode
|
from .dom import DOMNode
|
||||||
from .geometry import Offset, Region, Size
|
from .geometry import Offset, Region, Size
|
||||||
from .reactive import Reactive
|
from .reactive import Reactive
|
||||||
@@ -93,6 +94,13 @@ class ResultCallback(Generic[ScreenResultType]):
|
|||||||
class Screen(Generic[ScreenResultType], Widget):
|
class Screen(Generic[ScreenResultType], Widget):
|
||||||
"""The base class for screens."""
|
"""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 = """
|
DEFAULT_CSS = """
|
||||||
Screen {
|
Screen {
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
@@ -100,7 +108,6 @@ class Screen(Generic[ScreenResultType], Widget):
|
|||||||
background: $surface;
|
background: $surface;
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
focused: Reactive[Widget | None] = Reactive(None)
|
focused: Reactive[Widget | None] = Reactive(None)
|
||||||
"""The focused [widget][textual.widget.Widget] or `None` for no focus."""
|
"""The focused [widget][textual.widget.Widget] or `None` for no focus."""
|
||||||
stack_updates: Reactive[int] = Reactive(0, repaint=False)
|
stack_updates: Reactive[int] = Reactive(0, repaint=False)
|
||||||
@@ -659,6 +666,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()
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +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
|
||||||
|
|
||||||
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,
|
||||||
@@ -150,3 +151,44 @@ async def test_screens():
|
|||||||
screen2.remove()
|
screen2.remove()
|
||||||
screen3.remove()
|
screen3.remove()
|
||||||
await app._shutdown()
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user