diff --git a/CHANGELOG.md b/CHANGELOG.md
index c45cbb4163..49fc6e7f18 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added `grid_size` property to `GridLayout` https://github.com/Textualize/textual/pull/6210
- Exposed `NoSelection` and `BLANK` via `textual.widgets.select` https://github.com/Textualize/textual/pull/6214
+- Added `Widget.FOCUS_ON_CLICK` classvar amd `Widget.focus_on_click` method https://github.com/Textualize/textual/pull/6216
### Changed
diff --git a/src/textual/app.py b/src/textual/app.py
index 534f38e6a9..9bf7ad11a1 100644
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -2437,8 +2437,8 @@ def mount(
MountError: If there is a problem with the mount request.
Note:
- Only one of ``before`` or ``after`` can be provided. If both are
- provided a ``MountError`` will be raised.
+ Only one of `before` or `after` can be provided. If both are
+ provided a `MountError` will be raised.
"""
return self.screen.mount(*widgets, before=before, after=after)
@@ -2467,8 +2467,8 @@ def mount_all(
MountError: If there is a problem with the mount request.
Note:
- Only one of ``before`` or ``after`` can be provided. If both are
- provided a ``MountError`` will be raised.
+ Only one of `before` or `after` can be provided. If both are
+ provided a `MountError` will be raised.
"""
return self.mount(*widgets, before=before, after=after)
diff --git a/src/textual/screen.py b/src/textual/screen.py
index 8af7c2f695..bc6f14207f 100644
--- a/src/textual/screen.py
+++ b/src/textual/screen.py
@@ -1722,7 +1722,10 @@ def _forward_event(self, event: events.Event) -> None:
else:
if isinstance(event, events.MouseDown):
focusable_widget = self.get_focusable_widget_at(event.x, event.y)
- if focusable_widget:
+ if (
+ focusable_widget is not None
+ and focusable_widget.focus_on_click()
+ ):
self.set_focus(focusable_widget, scroll_visible=False)
event.style = self.get_style_at(event.screen_x, event.screen_y)
if widget.loading:
diff --git a/src/textual/widget.py b/src/textual/widget.py
index d02bb237fe..ad526e606e 100644
--- a/src/textual/widget.py
+++ b/src/textual/widget.py
@@ -324,7 +324,10 @@ class Widget(DOMNode):
"""
ALLOW_SELECT: ClassVar[bool] = True
- """Does this widget support automatic text selection? May be further refined with [Widget.allow_select][textual.widget.Widget.allow_select]"""
+ """Does this widget support automatic text selection? May be further refined with [Widget.allow_select][textual.widget.Widget.allow_select]."""
+
+ FOCUS_ON_CLICK: ClassVar[bool] = True
+ """Should focusable widgets be automatically focused on click? Default return value of [Widget.focus_on_click][textual.widget.Widget.focus_on_click]."""
can_focus: bool = False
"""Widget may receive focus."""
@@ -679,6 +682,17 @@ def text_selection(self) -> Selection | None:
"""Text selection information, or `None` if no text is selected in this widget."""
return self.screen.selections.get(self, None)
+ def focus_on_click(self) -> bool:
+ """Automatically focus the widget on click?
+
+ Implement this if you want to change the default click to focus behavior.
+ The default will return the classvar `FOCUS_ON_CLICK`.
+
+ Returns:
+ `True` if Textual should set focus automatically on a click, or `False` if it shouldn't.
+ """
+ return self.FOCUS_ON_CLICK
+
def get_line_filters(self) -> Sequence[LineFilter]:
"""Get the line filters enabled for this widget.
@@ -1470,14 +1484,58 @@ def mount_all(
MountError: If there is a problem with the mount request.
Note:
- Only one of ``before`` or ``after`` can be provided. If both are
- provided a ``MountError`` will be raised.
+ Only one of `before` or `after` can be provided. If both are
+ provided a `MountError` will be raised.
"""
if self.app._exit:
return AwaitMount(self, [])
await_mount = self.mount(*widgets, before=before, after=after)
return await_mount
+ def mount_compose(
+ self,
+ compose_result: ComposeResult,
+ *,
+ before: int | str | Widget | None = None,
+ after: int | str | Widget | None = None,
+ ) -> AwaitMount:
+ """Mount widgets from the result of a compose method.
+
+ Example:
+ ```python
+ def on_key(self, event:events.Key) -> None:
+
+ def add_key(key:str) -> ComposeResult:
+ '''Compose key information widgets'''
+ with containers.HorizontalGroup():
+ yield Label("You pressed:")
+ yield Label(key)
+
+ self.mount_compose(add_key(event.key))
+
+ ```
+
+ Args:
+ compose_result: The result of a compose method.
+ before: Optional location to mount before. An `int` is the index
+ of the child to mount before, a `str` is a `query_one` query to
+ find the widget to mount before.
+ after: Optional location to mount after. An `int` is the index
+ of the child to mount after, a `str` is a `query_one` query to
+ find the widget to mount after.
+
+ Returns:
+ An awaitable object that waits for widgets to be mounted.
+
+ Raises:
+ MountError: If there is a problem with the mount request.
+
+ Note:
+ Only one of `before` or `after` can be provided. If both are
+ provided a `MountError` will be raised.
+ """
+ return self.mount_all(compose(self, compose_result), before=before, after=after)
+
if TYPE_CHECKING:
@overload
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_focus_on_click.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_focus_on_click.svg
new file mode 100644
index 0000000000..fcf89ec8b1
--- /dev/null
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_focus_on_click.svg
@@ -0,0 +1,152 @@
+
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_mount_compose.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_mount_compose.svg
new file mode 100644
index 0000000000..09a169e4d8
--- /dev/null
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_mount_compose.svg
@@ -0,0 +1,150 @@
+
diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py
index 0f99dbaf51..bcb3cb93fb 100644
--- a/tests/snapshot_tests/test_snapshots.py
+++ b/tests/snapshot_tests/test_snapshots.py
@@ -4739,3 +4739,45 @@ async def action_clear(self) -> None:
await vs.remove_children()
assert snap_compare(PruneApp(), press=["c"])
+
+
+def test_focus_on_click(snap_compare) -> None:
+ """Test focus on click may be prevented.
+
+ You should see a button in a non-focused stated
+
+ """
+
+ class NonFocusButton(Button):
+ FOCUS_ON_CLICK = False
+
+ class FocusApp(App):
+ AUTO_FOCUS = None
+
+ def compose(self) -> ComposeResult:
+ yield NonFocusButton("Click")
+
+ async def run_before(pilot: Pilot) -> None:
+ await pilot.pause()
+ await pilot.click(NonFocusButton)
+
+ assert snap_compare(FocusApp(), run_before=run_before)
+
+
+def test_mount_compose(snap_compare) -> None:
+ """Test the `Widget.mount_compose` method.
+
+ You should see a Hello World message.
+ """
+
+ class ComposeApp(App):
+
+ async def on_mount(self) -> None:
+ # Perform compose outside of usual compose method.
+ def compose_things() -> ComposeResult:
+ """Add a label."""
+ yield Label("Hello, World!")
+
+ await self.screen.mount_compose(compose_things())
+
+ assert snap_compare(ComposeApp())