Skip to content

Commit 976bd2f

Browse files
authored
Move DataTable cursor with page up/down, home, end (#2228)
* Add pageup and pagedown actions to DataTable, with no impls * Pagedown moves DataTable cursor now * Account for header height in pagedown action * Page Up support in the DataTable * Fix and off-by-1, ensure page up/down works on col cursor * Add placeholder scroll home/end action handlers to datatable * Add scroll home and scroll end * Hide hover cursor when home or end is used * Ensure home and end work correctly with all curosrs * Testing home/end/pagedown/pageup cursor movement in DataTable * Docstrings for new datatable actions * Fix a broken unit test for the DataTable
1 parent c3e56f1 commit 976bd2f

File tree

2 files changed

+100
-2
lines changed

2 files changed

+100
-2
lines changed

src/textual/widgets/_data_table.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
203203
Binding("down", "cursor_down", "Cursor Down", show=False),
204204
Binding("right", "cursor_right", "Cursor Right", show=False),
205205
Binding("left", "cursor_left", "Cursor Left", show=False),
206+
Binding("pageup", "page_up", "Page Up", show=False),
207+
Binding("pagedown", "page_down", "Page Down", show=False),
206208
]
207209
"""
208210
| Key(s) | Description |
@@ -247,7 +249,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
247249
background: $surface ;
248250
color: $text;
249251
height: auto;
250-
max-height: 100vh
252+
max-height: 100vh;
251253
}
252254
DataTable > .datatable--header {
253255
text-style: bold;
@@ -1976,6 +1978,76 @@ def on_click(self, event: events.Click) -> None:
19761978
self._scroll_cursor_into_view(animate=True)
19771979
event.stop()
19781980

1981+
def action_page_down(self) -> None:
1982+
"""Move the cursor one page down."""
1983+
self._set_hover_cursor(False)
1984+
cursor_type = self.cursor_type
1985+
if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"):
1986+
height = self.size.height - self.header_height if self.show_header else 0
1987+
1988+
# Determine how many rows constitutes a "page"
1989+
offset = 0
1990+
rows_to_scroll = 0
1991+
row_index, column_index = self.cursor_coordinate
1992+
for ordered_row in self.ordered_rows[row_index:]:
1993+
offset += ordered_row.height
1994+
if offset > height:
1995+
break
1996+
rows_to_scroll += 1
1997+
1998+
self.cursor_coordinate = Coordinate(
1999+
row_index + rows_to_scroll - 1, column_index
2000+
)
2001+
self._scroll_cursor_into_view()
2002+
else:
2003+
super().action_page_down()
2004+
2005+
def action_page_up(self) -> None:
2006+
"""Move the cursor one page up."""
2007+
self._set_hover_cursor(False)
2008+
cursor_type = self.cursor_type
2009+
if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"):
2010+
height = self.size.height - self.header_height if self.show_header else 0
2011+
2012+
# Determine how many rows constitutes a "page"
2013+
offset = 0
2014+
rows_to_scroll = 0
2015+
row_index, column_index = self.cursor_coordinate
2016+
for ordered_row in self.ordered_rows[: row_index + 1]:
2017+
offset += ordered_row.height
2018+
if offset > height:
2019+
break
2020+
rows_to_scroll += 1
2021+
2022+
self.cursor_coordinate = Coordinate(
2023+
row_index - rows_to_scroll + 1, column_index
2024+
)
2025+
self._scroll_cursor_into_view()
2026+
else:
2027+
super().action_page_up()
2028+
2029+
def action_scroll_home(self) -> None:
2030+
"""Scroll to the top of the data table."""
2031+
self._set_hover_cursor(False)
2032+
cursor_type = self.cursor_type
2033+
if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"):
2034+
row_index, column_index = self.cursor_coordinate
2035+
self.cursor_coordinate = Coordinate(0, column_index)
2036+
self._scroll_cursor_into_view()
2037+
else:
2038+
super().action_scroll_home()
2039+
2040+
def action_scroll_end(self) -> None:
2041+
"""Scroll to the bottom of the data table."""
2042+
self._set_hover_cursor(False)
2043+
cursor_type = self.cursor_type
2044+
if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"):
2045+
row_index, column_index = self.cursor_coordinate
2046+
self.cursor_coordinate = Coordinate(self.row_count - 1, column_index)
2047+
self._scroll_cursor_into_view()
2048+
else:
2049+
super().action_scroll_end()
2050+
19792051
def action_cursor_up(self) -> None:
19802052
self._set_hover_cursor(False)
19812053
cursor_type = self.cursor_type

tests/test_data_table.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,36 @@ async def test_datatable_message_emission():
167167
async def test_empty_table_interactions():
168168
app = DataTableApp()
169169
async with app.run_test() as pilot:
170-
await pilot.press("enter", "up", "down", "left", "right")
170+
await pilot.press(
171+
"enter", "up", "down", "left", "right", "home", "end", "pagedown", "pageup"
172+
)
171173
assert app.message_names == []
172174

173175

176+
async def test_cursor_movement_with_home_pagedown_etc():
177+
app = DataTableApp()
178+
179+
async with app.run_test() as pilot:
180+
table = app.query_one(DataTable)
181+
table.add_columns("A", "B")
182+
table.add_rows(ROWS)
183+
await pilot.press("right", "pagedown")
184+
await pilot.pause()
185+
assert table.cursor_coordinate == Coordinate(2, 1)
186+
187+
await pilot.press("pageup")
188+
await pilot.pause()
189+
assert table.cursor_coordinate == Coordinate(0, 1)
190+
191+
await pilot.press("end")
192+
await pilot.pause()
193+
assert table.cursor_coordinate == Coordinate(2, 1)
194+
195+
await pilot.press("home")
196+
await pilot.pause()
197+
assert table.cursor_coordinate == Coordinate(0, 1)
198+
199+
174200
async def test_add_rows():
175201
app = DataTableApp()
176202
async with app.run_test():

0 commit comments

Comments
 (0)