diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14e1bd2dd9..b4ec7ccd57 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
+## Unreleased
+
+### [7.5.0] - 2026-01-29
+
+- The DataTable row cursor will extend to the full width if there is excess space https://github.com/Textualize/textual/pull/6345
+- The DataTable will send a selected event on click, only if the cell / row / column is currently highlighted https://github.com/Textualize/textual/pull/6345
+
## [7.4.0] - 2026-01-25
### Added
@@ -3328,6 +3335,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
- New handler system for messages that doesn't require inheritance
- Improved traceback handling
+[7.5.0]: https://github.com/Textualize/textual/compare/v7.4.0...v7.5.0
[7.4.0]: https://github.com/Textualize/textual/compare/v7.3.0...v7.4.0
[7.3.0]: https://github.com/Textualize/textual/compare/v7.2.0...v7.3.0
[7.2.0]: https://github.com/Textualize/textual/compare/v7.1.0...v7.2.0
diff --git a/pyproject.toml b/pyproject.toml
index 02d2ee9c33..9479fc774d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual"
-version = "7.4.0"
+version = "7.5.0"
homepage = "https://github.com/Textualize/textual"
repository = "https://github.com/Textualize/textual"
documentation = "https://textual.textualize.io/"
diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py
index 04e9b0599e..dd5f9c6d72 100644
--- a/src/textual/widgets/_data_table.py
+++ b/src/textual/widgets/_data_table.py
@@ -268,6 +268,8 @@ class RowRenderables(NamedTuple):
class DataTable(ScrollView, Generic[CellType], can_focus=True):
"""A tabular widget that contains data."""
+ ALLOW_SELECT = False
+
BINDINGS: ClassVar[list[BindingType]] = [
Binding("enter", "select_cursor", "Select", show=False),
Binding("up", "cursor_up", "Cursor up", show=False),
@@ -328,7 +330,6 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
color: $foreground;
height: auto;
max-height: 100%;
-
&.datatable--fixed-cursor {
background: $block-cursor-blurred-background;
}
@@ -450,7 +451,7 @@ def __init__(
) -> None:
self.data_table = data_table
"""The data table."""
- self.value: CellType = value
+ self.value = value
"""The value in the highlighted cell."""
self.coordinate: Coordinate = coordinate
"""The coordinate of the highlighted cell."""
@@ -1555,7 +1556,7 @@ def _get_row_region(self, row_index: int) -> Region:
y = sum(ordered_row.height for ordered_row in self.ordered_rows[:row_index])
if self.show_header:
y += self.header_height
- row_region = Region(0, y, row_width, row.height)
+ row_region = Region(0, y, max(self.size.width, row_width), row.height)
return row_region
def _get_column_region(self, column_index: int) -> Region:
@@ -2357,17 +2358,36 @@ def _render_line_in_row(
)
remaining_space = max(0, widget_width - table_width)
background_color = self.background_colors[1]
- if row_style.bgcolor is not None:
- # TODO: This should really be in a component class
- faded_color = Color.from_rich_color(row_style.bgcolor).blend(
- background_color, factor=0.25
- )
- faded_style = Style.from_color(
- color=row_style.color, bgcolor=faded_color.rich_color
+ if self.cursor_type == "row":
+ extend_style, _ = self._get_styles_to_render_cell(
+ row_index == -1,
+ False,
+ False,
+ should_highlight(
+ hover_location, Coordinate(row_index or 0, 0), cursor_type
+ ),
+ row_index == cursor_location.row,
+ self.show_cursor,
+ self._show_hover_cursor,
+ False,
+ False,
)
+ extend_style = row_style + extend_style
else:
- faded_style = Style.from_color(row_style.color, row_style.bgcolor)
- scrollable_row.append([Segment(" " * remaining_space, faded_style)])
+ if row_style.bgcolor is not None:
+ # TODO: This should really be in a component class
+ faded_color = Color.from_rich_color(row_style.bgcolor).blend(
+ background_color, factor=0.25
+ )
+ extend_style = Style.from_color(
+ color=row_style.color, bgcolor=faded_color.rich_color
+ )
+ else:
+ extend_style = Style.from_color(row_style.color, row_style.bgcolor)
+ extend_style += Style.from_meta(
+ {"row": row_index, "column": 0, "out_of_bounds": True}
+ )
+ scrollable_row.append([Segment(" " * remaining_space, extend_style)])
row_pair = (fixed_row, scrollable_row)
self._row_render_cache[cache_key] = row_pair
@@ -2389,7 +2409,7 @@ def _get_offsets(self, y: int) -> tuple[RowKey, int]:
return self._header_row_key, y
y -= header_height
if y > len(y_offsets):
- raise LookupError("Y coord {y!r} is greater than total height")
+ raise LookupError(f"Y coord {y!r} is greater than total height")
return y_offsets[y]
@@ -2541,6 +2561,10 @@ def _on_mouse_move(self, event: events.MouseMove):
self._set_hover_cursor(False)
return
+ if self.cursor_type != "row" and meta.get("out_of_bounds", False):
+ self._set_hover_cursor(False)
+ return
+
if self.show_cursor and self.cursor_type != "none":
try:
self.hover_coordinate = Coordinate(meta["row"], meta["column"])
@@ -2647,6 +2671,8 @@ async def _on_click(self, event: events.Click) -> None:
meta = event.style.meta
if "row" not in meta or "column" not in meta:
return
+ if self.cursor_type != "row" and meta.get("out_of_bounds", False):
+ return
row_index = meta["row"]
column_index = meta["column"]
@@ -2667,8 +2693,11 @@ async def _on_click(self, event: events.Click) -> None:
self.post_message(message)
elif self.show_cursor and self.cursor_type != "none":
# Only post selection events if there is a visible row/col/cell cursor.
- self.cursor_coordinate = Coordinate(row_index, column_index)
- self._post_selected_message()
+ new_coordinate = Coordinate(row_index, column_index)
+ highlight_click = new_coordinate == self.cursor_coordinate
+ self.cursor_coordinate = new_coordinate
+ if highlight_click:
+ self._post_selected_message()
self._scroll_cursor_into_view(animate=True)
event.stop()
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_hot_reloading.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_hot_reloading.svg
index 77272ea020..24e5fcd323 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_hot_reloading.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_hot_reloading.svg
@@ -19,133 +19,133 @@
font-weight: 700;
}
- .terminal-1293670551-matrix {
+ .terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-1293670551-title {
+ .terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-1293670551-r1 { fill: #e0e0e0;font-weight: bold }
-.terminal-1293670551-r2 { fill: #e0e0e0 }
-.terminal-1293670551-r3 { fill: #c5c8c6 }
-.terminal-1293670551-r4 { fill: #ddedf9;font-weight: bold }
+ .terminal-r1 { fill: #e0e0e0;font-weight: bold }
+.terminal-r2 { fill: #c5c8c6 }
+.terminal-r3 { fill: #ddedf9;font-weight: bold }
+.terminal-r4 { fill: #e0e0e0 }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- DataTableHotReloadingApp
+ DataTableHotReloadingApp
-
-
-
- A B
- one two
- three four
- five six
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ A B
+ one two
+ three four
+ five six
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_row_cursor_render.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_row_cursor_render.svg
index c207da3cb4..26d705a735 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_row_cursor_render.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_row_cursor_render.svg
@@ -19,133 +19,133 @@
font-weight: 700;
}
- .terminal-2406117197-matrix {
+ .terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-2406117197-title {
+ .terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-2406117197-r1 { fill: #e0e0e0;font-weight: bold }
-.terminal-2406117197-r2 { fill: #e0e0e0 }
-.terminal-2406117197-r3 { fill: #c5c8c6 }
-.terminal-2406117197-r4 { fill: #ddedf9;font-weight: bold }
+ .terminal-r1 { fill: #e0e0e0;font-weight: bold }
+.terminal-r2 { fill: #c5c8c6 }
+.terminal-r3 { fill: #e0e0e0 }
+.terminal-r4 { fill: #ddedf9;font-weight: bold }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- TableApp
+ TableApp
-
-
-
- lane swimmer country time
- 4 Joseph Schooling Singapore 50.39
- 2 Michael Phelps United States 51.14
- 5 Chad le Clos South Africa 51.14
- 6 László Cseh Hungary 51.14
- 3 Li Zhuhao China 51.26
- 8 Mehdy Metella France 51.58
- 7 Tom Shields United States 51.73
- 1 Aleksandr Sadovnikov Russia 51.84
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ lane swimmer country time
+ 4 Joseph Schooling Singapore 50.39
+ 2 Michael Phelps United States 51.14
+ 5 Chad le Clos South Africa 51.14
+ 6 László Cseh Hungary 51.14
+ 3 Li Zhuhao China 51.26
+ 8 Mehdy Metella France 51.58
+ 7 Tom Shields United States 51.73
+ 1 Aleksandr Sadovnikov Russia 51.84
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/test_data_table.py b/tests/test_data_table.py
index 63032c67b0..6750c5ddbc 100644
--- a/tests/test_data_table.py
+++ b/tests/test_data_table.py
@@ -29,6 +29,7 @@
class DataTableApp(App):
+ AUTO_FOCUS = None
messages_to_record = {
"CellHighlighted",
"CellSelected",
@@ -787,8 +788,8 @@ async def test_coordinate_to_cell_key_invalid_coordinate():
async def test_datatable_click_cell_cursor():
- """When the cell cursor is used, and we click, we emit a CellHighlighted
- *and* a CellSelected message for the cell that was clicked.
+ """When the cell cursor is used, and we click, we emit a CellHighlighted message,
+ a second click emits a CellSelected message.
Regression test for https://github.com/Textualize/textual/issues/1723"""
app = DataTableApp()
async with app.run_test() as pilot:
@@ -797,6 +798,7 @@ async def test_datatable_click_cell_cursor():
table.add_row("123")
row_key = table.add_row("456")
await pilot.click(offset=Offset(1, 2))
+ await pilot.click(offset=Offset(1, 2))
# There's two CellHighlighted events since a cell is highlighted on initial load,
# then when we click, another cell is highlighted (and selected).
assert app.message_names == [
@@ -825,15 +827,22 @@ async def test_click_row_cursor():
table.add_column("ABC")
table.add_row("123")
row_key = table.add_row("456")
+
await pilot.click(offset=Offset(1, 2))
- assert app.message_names == ["RowHighlighted", "RowHighlighted", "RowSelected"]
+ print(repr(app.message_names))
+ assert app.message_names == ["RowHighlighted", "RowHighlighted"]
row_highlighted: DataTable.RowHighlighted = app.messages[1]
assert row_highlighted.row_key == row_key
+
assert row_highlighted.cursor_row == 1
- row_selected: DataTable.RowSelected = app.messages[2]
+ app.messages.clear()
+ await pilot.click(offset=Offset(1, 2))
+ assert app.message_names == ["RowSelected"]
+
+ row_selected: DataTable.RowSelected = app.messages[0]
assert row_selected.row_key == row_key
assert row_highlighted.cursor_row == 1
@@ -849,6 +858,7 @@ async def test_click_column_cursor():
table.add_row("123")
table.add_row("456")
await pilot.click(offset=Offset(1, 2))
+ await pilot.click(offset=Offset(1, 2))
assert app.message_names == [
"ColumnHighlighted",
"ColumnHighlighted",