Repository: Textualize/textual

Issue:
KeyError on DataTable border title link click
1. Run the attached MRE
2. Click on the right bottom table subtitle link

```
╭────────────────────────────────────── Traceback (most recent call last) ───────────────────────────────────────╮
│ /Users/user/.virtualenvs/textual-db/lib/python3.12/site-packages/textual/widgets/_data_table.py:2447          │
│ in _on_click                                                                                                   │
│                                                                                                                │
│   2444 │   │   if not meta:                                                                                    │
│   2445 │   │   │   return                                                                                      │
│   2446 │   │                                                                                                   │
│ ❱ 2447 │   │   row_index = meta["row"]                                                                         │
│   2448 │   │   column_index = meta["column"]                                                                   │
│   2449 │   │   is_header_click = self.show_header and row_index == -1                                          │
│   2450 │   │   is_row_label_click = self.show_row_labels and column_index == -1                                │
│                                                                                                                │
│ ╭───────────────────── locals ─────────────────────╮                                                           │
│ │ event = Click(x=108, y=4, button=1)              │                                                           │
│ │  meta = {'@click': ('toggle_status', ())} │                                                           │
│ │  self = DataTable()                              │                                                           │
│ ╰──────────────────────────────────────────────────╯                                                           │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
KeyError: 'row'
```

```python
from textual.app import App, ComposeResult
from textual.widgets import DataTable, Footer

ROWS = [
    ("Job", "Status"),
    ("Job 1", "Running"),
    ("Job 2", "Finished"),
]


class TableApp(App):
    BINDINGS = [
        ("t", "toggle_status", "Toggle"),
    ]
    running = False

    def compose(self) -> ComposeResult:
        yield DataTable(cursor_type="row")
        yield Footer()

    def on_mount(self) -> None:
        table = self.query_one(DataTable)
        table.add_columns(*ROWS[0])
        table.styles.border = ('solid', 'red')
        self.update_jobs()

    def update_jobs(self):
        table = self.query_one(DataTable)
        table.clear()
        table.add_rows([r for r in ROWS[1:] if not self.running or r[1] == "Running"])
        table.border_subtitle = "[b][@click=toggle_status()]{}[/][/]".format("▶️  running" if self.running else "*️⃣  all")
        table.refresh(layout=True)

    def action_toggle_status(self):
        self.running = not self.running
        self.update_jobs()

app = TableApp()
if __name__ == "__main__":
    app.run()
```

<details><summary>textual diagnose</summary>

```
# Textual Diagnostics

## Versions

| Name    | Value  |
|---------|--------|
| Textual | 0.56.0 |
| Rich    | 13.7.1 |

## Python

| Name           | Value                                                   |
|----------------|---------------------------------------------------------|
| Version        | 3.12.0                                                  |
| Implementation | CPython                                                 |
| Compiler       | Clang 14.0.3 (clang-1403.0.22.14.1)                     |
| Executable     | /Users/user/.virtualenvs/textual-db/bin/python |

## Operating System

| Name    | Value                                                                                                  |
|---------|--------------------------------------------------------------------------------------------------------|
| System  | Darwin                                                                                                 |
| Release | 22.6.0                                                                                                 |
| Version | Darwin Kernel Version 22.6.0: Mon Feb 19 19:48:53 PST 2024; root:xnu-8796.141.3.704.6~1/RELEASE_X86_64 |

## Terminal

| Name                 | Value              |
|----------------------|--------------------|
| Terminal Application | iTerm.app (3.4.23) |
| TERM                 | xterm-256color     |
| COLORTERM            | truecolor          |
| FORCE_COLOR          | *Not set*          |
| NO_COLOR             | *Not set*          |

## Rich Console options

| Name           | Value                |
|----------------|----------------------|
| size           | width=114, height=55 |
| legacy_windows | False                |
| min_width      | 1                    |
| max_width      | 114                  |
| is_terminal    | False                |
| encoding       | utf-8                |
| max_height     | 55                   |
| justify        | None                 |
| overflow       | None                 |
| no_wrap        | False                |
| highlight      | None                 |
| markup         | None                 |
| height         | None                 |
```
</details> 


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e60932a11..6df2d6c3d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,9 +5,12 @@ 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
 
+### Fixed
+
+- Fixed a crash in `DataTable` if you clicked a link in the border https://github.com/Textualize/textual/issues/4410
+
 ### Added
 
 - Added `App.copy_to_clipboard` https://github.com/Textualize/textual/pull/4416
diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py
index 15d73097e..b695c8352 100644
--- a/src/textual/widgets/_data_table.py
+++ b/src/textual/widgets/_data_table.py
@@ -2441,7 +2441,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
     async def _on_click(self, event: events.Click) -> None:
         self._set_hover_cursor(True)
         meta = event.style.meta
-        if not meta:
+        if not "row" in meta or not "column" in meta:
             return
 
         row_index = meta["row"]


Test patch:
diff --git a/tests/test_data_table.py b/tests/test_data_table.py
index e09a9c3f9..cb921ae63 100644
--- a/tests/test_data_table.py
+++ b/tests/test_data_table.py
@@ -6,7 +6,7 @@ from rich.text import Text
 
 from textual._wait import wait_for_idle
 from textual.actions import SkipAction
-from textual.app import App, RenderableType
+from textual.app import App, ComposeResult, RenderableType
 from textual.coordinate import Coordinate
 from textual.geometry import Offset
 from textual.message import Message
@@ -1420,3 +1420,31 @@ async def test_move_cursor_respects_animate_parameter():
         await pilot.press("s")
 
     assert scrolls == [True, False]
+
+
+async def test_clicking_border_link_doesnt_crash():
+    """Regression test for https://github.com/Textualize/textual/issues/4410"""
+
+    class DataTableWithBorderLinkApp(App):
+        CSS = """
+        DataTable {
+            border: solid red;
+        }
+        """
+        link_clicked = False
+
+        def compose(self) -> ComposeResult:
+            yield DataTable()
+
+        def on_mount(self) -> None:
+            table = self.query_one(DataTable)
+            table.border_title = "[@click=test_link]Border Link[/]"
+
+        def action_test_link(self) -> None:
+            self.link_clicked = True
+
+    app = DataTableWithBorderLinkApp()
+    async with app.run_test() as pilot:
+        # Test clicking the link in the border doesn't crash with KeyError: 'row'
+        await pilot.click(DataTable, offset=(5, 0))
+        assert app.link_clicked is True