diff --git a/CHANGELOG.md b/CHANGELOG.md
index cdd4b7f829..0856eca6c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,8 +20,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- grid-columns and grid-rows now accept an `auto` token to detect the optimal size https://github.com/Textualize/textual/pull/3107
+### Fixed
+
+- Fixed auto height container with default grid-rows https://github.com/Textualize/textual/issues/1597
+- Fixed `page_up` and `page_down` bug in `DataTable` when `show_header = False` https://github.com/Textualize/textual/pull/3093
+
## [0.33.0] - 2023-08-15
+
### Fixed
- Fixed unintuitive sizing behaviour of TabbedContent https://github.com/Textualize/textual/issues/2411
@@ -58,6 +64,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added App.begin_capture_print, App.end_capture_print, Widget.begin_capture_print, Widget.end_capture_print https://github.com/Textualize/textual/issues/2952
- Added the ability to run async methods as thread workers https://github.com/Textualize/textual/pull/2938
+- Added `ListView.extend` method to append multiple items https://github.com/Textualize/textual/pull/3012
- Added `App.stop_animation` https://github.com/Textualize/textual/issues/2786
- Added `Widget.stop_animation` https://github.com/Textualize/textual/issues/2786
diff --git a/docs/examples/widgets/header_app_title.py b/docs/examples/widgets/header_app_title.py
new file mode 100644
index 0000000000..1b433148e2
--- /dev/null
+++ b/docs/examples/widgets/header_app_title.py
@@ -0,0 +1,16 @@
+from textual.app import App, ComposeResult
+from textual.widgets import Header
+
+
+class HeaderApp(App):
+ def compose(self) -> ComposeResult:
+ yield Header()
+
+ def on_mount(self) -> None:
+ self.title = "Header Application"
+ self.sub_title = "With title and sub-title"
+
+
+if __name__ == "__main__":
+ app = HeaderApp()
+ app.run()
diff --git a/docs/widgets/header.md b/docs/widgets/header.md
index 3286b3e8c1..c589ddcf00 100644
--- a/docs/widgets/header.md
+++ b/docs/widgets/header.md
@@ -2,6 +2,10 @@
A simple header widget which docks itself to the top of the parent container.
+!!! note
+
+ The application title which is shown in the header is taken from the [`title`][textual.app.App.title] and [`sub_title`][textual.app.App.sub_title] of the application.
+
- [ ] Focusable
- [ ] Container
@@ -20,6 +24,19 @@ The example below shows an app with a `Header`.
--8<-- "docs/examples/widgets/header.py"
```
+This example shows how to set the text in the `Header` using `App.title` and `App.sub_title`:
+
+=== "Output"
+
+ ```{.textual path="docs/examples/widgets/header_app_title.py"}
+ ```
+
+=== "header_app_title.py"
+
+ ```python
+ --8<-- "docs/examples/widgets/header_app_title.py"
+ ```
+
## Reactive Attributes
| Name | Type | Default | Description |
diff --git a/src/textual/demo.py b/src/textual/demo.py
index df63b53235..0ec59483f2 100644
--- a/src/textual/demo.py
+++ b/src/textual/demo.py
@@ -7,6 +7,7 @@
from rich.console import RenderableType
from rich.json import JSON
from rich.markdown import Markdown
+from rich.markup import escape
from rich.pretty import Pretty
from rich.syntax import Syntax
from rich.table import Table
@@ -269,14 +270,6 @@ class SubTitle(Static):
pass
-class Notification(Static):
- def on_mount(self) -> None:
- self.set_timer(3, self.remove)
-
- def on_click(self) -> None:
- self.remove()
-
-
class DemoApp(App[None]):
CSS_PATH = "demo.css"
TITLE = "Textual Demo"
@@ -285,7 +278,7 @@ class DemoApp(App[None]):
("ctrl+t", "app.toggle_dark", "Toggle Dark mode"),
("ctrl+s", "app.screenshot()", "Screenshot"),
("f1", "app.toggle_class('RichLog', '-hidden')", "Notes"),
- Binding("ctrl+c,ctrl+q", "app.quit", "Quit", show=True),
+ Binding("ctrl+q", "app.quit", "Quit", show=True),
]
show_sidebar = reactive(False)
@@ -390,9 +383,9 @@ def action_screenshot(self, filename: str | None = None, path: str = "./") -> No
"""
self.bell()
path = self.save_screenshot(filename, path)
- message = Text.assemble("Screenshot saved to ", (f"'{path}'", "bold green"))
- self.add_note(message)
- self.screen.mount(Notification(message))
+ message = f"Screenshot saved to [bold green]'{escape(str(path))}'[/]"
+ self.add_note(Text.from_markup(message))
+ self.notify(message)
app = DemoApp()
diff --git a/src/textual/events.py b/src/textual/events.py
index 0a24c034e1..94e13df6f5 100644
--- a/src/textual/events.py
+++ b/src/textual/events.py
@@ -535,7 +535,7 @@ class Leave(Event, bubble=False, verbose=True):
class Focus(Event, bubble=False):
"""Sent when a widget is focussed.
- - [X] Bubbles
+ - [ ] Bubbles
- [ ] Verbose
"""
@@ -543,7 +543,7 @@ class Focus(Event, bubble=False):
class Blur(Event, bubble=False):
"""Sent when a widget is blurred (un-focussed).
- - [X] Bubbles
+ - [ ] Bubbles
- [ ] Verbose
"""
diff --git a/src/textual/layouts/grid.py b/src/textual/layouts/grid.py
index c1bb3ff47e..3ef2e315f8 100644
--- a/src/textual/layouts/grid.py
+++ b/src/textual/layouts/grid.py
@@ -21,7 +21,9 @@ def arrange(
self, parent: Widget, children: list[Widget], size: Size
) -> ArrangeResult:
styles = parent.styles
- row_scalars = styles.grid_rows or [Scalar.parse("1fr")]
+ row_scalars = styles.grid_rows or (
+ [Scalar.parse("1fr")] if size.height else [Scalar.parse("auto")]
+ )
column_scalars = styles.grid_columns or [Scalar.parse("1fr")]
gutter_horizontal = styles.grid_gutter_horizontal
gutter_vertical = styles.grid_gutter_vertical
diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py
index db3b2801d4..6201983dd6 100644
--- a/src/textual/widgets/_data_table.py
+++ b/src/textual/widgets/_data_table.py
@@ -2205,9 +2205,8 @@ async def _on_click(self, event: events.Click) -> None:
def action_page_down(self) -> None:
"""Move the cursor one page down."""
self._set_hover_cursor(False)
- cursor_type = self.cursor_type
- if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"):
- height = self.size.height - self.header_height if self.show_header else 0
+ if self.show_cursor and self.cursor_type in ("cell", "row"):
+ height = self.size.height - (self.header_height if self.show_header else 0)
# Determine how many rows constitutes a "page"
offset = 0
@@ -2228,9 +2227,8 @@ def action_page_down(self) -> None:
def action_page_up(self) -> None:
"""Move the cursor one page up."""
self._set_hover_cursor(False)
- cursor_type = self.cursor_type
- if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"):
- height = self.size.height - self.header_height if self.show_header else 0
+ if self.show_cursor and self.cursor_type in ("cell", "row"):
+ height = self.size.height - (self.header_height if self.show_header else 0)
# Determine how many rows constitutes a "page"
offset = 0
diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py
index f9f423b53a..a3bf67b896 100644
--- a/src/textual/widgets/_list_view.py
+++ b/src/textual/widgets/_list_view.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import ClassVar, Optional
+from typing import ClassVar, Iterable, Optional
from textual.await_remove import AwaitRemove
from textual.binding import Binding, BindingType
@@ -172,6 +172,21 @@ def watch_index(self, old_index: int, new_index: int) -> None:
self._scroll_highlighted_region()
self.post_message(self.Highlighted(self, new_child))
+ def extend(self, items: Iterable[ListItem]) -> AwaitMount:
+ """Append multiple new ListItems to the end of the ListView.
+
+ Args:
+ items: The ListItems to append.
+
+ Returns:
+ An awaitable that yields control to the event loop
+ until the DOM has been updated with the new child items.
+ """
+ await_mount = self.mount(*items)
+ if len(self) == 1:
+ self.index = 0
+ return await_mount
+
def append(self, item: ListItem) -> AwaitMount:
"""Append a new ListItem to the end of the ListView.
@@ -182,10 +197,7 @@ def append(self, item: ListItem) -> AwaitMount:
An awaitable that yields control to the event loop
until the DOM has been updated with the new child item.
"""
- await_mount = self.mount(item)
- if len(self) == 1:
- self.index = 0
- return await_mount
+ return self.extend([item])
def clear(self) -> AwaitRemove:
"""Clear all items from the ListView.
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
index f68d2917b0..4fe8a4f049 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -484,6 +484,165 @@
'''
# ---
+# name: test_auto_grid_default_height
+ '''
+
+
+ '''
+# ---
# name: test_auto_table
'''
diff --git a/tests/snapshot_tests/snapshot_apps/auto_grid_default_height.py b/tests/snapshot_tests/snapshot_apps/auto_grid_default_height.py
new file mode 100644
index 0000000000..1b5883466e
--- /dev/null
+++ b/tests/snapshot_tests/snapshot_apps/auto_grid_default_height.py
@@ -0,0 +1,58 @@
+from __future__ import annotations
+
+from typing import Type
+from textual.app import App, ComposeResult
+from textual.containers import Container, Horizontal, Vertical, Grid
+from textual.widgets import Header, Footer, Label
+from textual.binding import Binding
+
+
+class GridHeightAuto(App[None]):
+ CSS = """
+ #test-area {
+
+ border: solid red;
+ height: auto;
+ }
+
+ Grid {
+ grid-size: 3;
+ # grid-rows: auto;
+ }
+ """
+
+ BINDINGS = [
+ Binding("g", "grid", "Grid"),
+ Binding("v", "vertical", "Vertical"),
+ Binding("h", "horizontal", "Horizontal"),
+ Binding("c", "container", "Container"),
+ ]
+
+ def compose(self) -> ComposeResult:
+ yield Header()
+ yield Vertical(Label("Select a container to test (see footer)"), id="sandbox")
+ yield Footer()
+
+ def build(self, out_of: Type[Container | Grid | Horizontal | Vertical]) -> None:
+ self.query("#sandbox > *").remove()
+ self.query_one("#sandbox", Vertical).mount(
+ Label("Here is some text before the grid"),
+ out_of(*[Label(f"Cell #{n}") for n in range(9)], id="test-area"),
+ Label("Here is some text after the grid"),
+ )
+
+ def action_grid(self):
+ self.build(Grid)
+
+ def action_vertical(self):
+ self.build(Vertical)
+
+ def action_horizontal(self):
+ self.build(Horizontal)
+
+ def action_container(self):
+ self.build(Container)
+
+
+if __name__ == "__main__":
+ GridHeightAuto().run()
diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py
index 5ad871c740..36b003bb7e 100644
--- a/tests/snapshot_tests/test_snapshots.py
+++ b/tests/snapshot_tests/test_snapshots.py
@@ -641,3 +641,7 @@ def test_digits(snap_compare) -> None:
def test_auto_grid(snap_compare) -> None:
assert snap_compare(SNAPSHOT_APPS_DIR / "auto_grid.py")
+
+
+def test_auto_grid_default_height(snap_compare) -> None:
+ assert snap_compare(SNAPSHOT_APPS_DIR / "auto_grid_default_height.py", press=["g"])
diff --git a/tests/test_data_table.py b/tests/test_data_table.py
index a1c08edc7c..e00b9432a4 100644
--- a/tests/test_data_table.py
+++ b/tests/test_data_table.py
@@ -173,11 +173,13 @@ async def test_empty_table_interactions():
assert app.message_names == []
-async def test_cursor_movement_with_home_pagedown_etc():
+@pytest.mark.parametrize("show_header", [True, False])
+async def test_cursor_movement_with_home_pagedown_etc(show_header):
app = DataTableApp()
async with app.run_test() as pilot:
table = app.query_one(DataTable)
+ table.show_header = show_header
table.add_columns("A", "B")
table.add_rows(ROWS)
await pilot.press("right", "pagedown")