Repository: Textualize/textual

Issue:
Populating a `ListView` via constructor vs via inherit and `compose`
There seems to be a subtle difference in the working of a `ListView` if you create one by passing the `ListItem`s to it, vs if you create a custom `ListView` by inheriting from it and using `compose` to populate it. Take the following example code, which places both approaches side-by-side:

```python
from textual.app        import App, ComposeResult
from textual.containers import Horizontal
from textual.widgets    import Header, Footer, ListView, ListItem, Label

class CustomListView( ListView ):

    def __init__( self, choices: list[ str ] ) -> None:
        super().__init__()
        self._choices = choices

    def compose( self ) -> ComposeResult:
        """Compose the child widgets."""
        for choice in self._choices:
            yield ListItem( Label( choice ) )

class ListViewMakerApp( App[ None ] ):

    CSS = """
    ListView {
        width: 1fr;
        height: 1fr;
        border: round red;
    }
    """

    OPTIONS = [ f"This is the nifty test option {n}" for n in range( 20 ) ]

    def compose( self ) -> ComposeResult:
        yield Header()
        yield Horizontal(
            ListView(
                *[ ListItem( Label( option ) ) for option in self.OPTIONS ]
            ),
            CustomListView( self.OPTIONS )
        )
        yield Footer()

if __name__ == "__main__":
    ListViewMakerApp().run()
```

I feel the `CustomListView` would not be an unusual approach for people to take; perhaps wanting to make their own commonly-used selection list of simple values (or even very complex child values that they want easily handled). Side-by-side the `ListView`s look the same to start with:

<img width="922" alt="Screenshot 2023-01-17 at 11 10 47" src="https://user-images.githubusercontent.com/28237/212884262-8d5252a8-3318-4f72-a3de-ae5f73b5a41c.png">

Note that the non-inherited `ListView` is to the left and that, even though it doesn't have focus, the first item is highlighted. If you <kbd>tab</kbd> into it you can cursor around, etc, just fine. On the other hand notice that the right `ListView` (which is a `CustomListView`) has no obvious highlighted item and if you tab into it nothing gets highlighted.

Further to this, if you (via keyboard) focus the inherited (right hand) `ListView` and then attempt to cursor down, the following exception will be raised:

```
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /Users/davep/develop/python/textual-sandbox/.venv/lib/python3.10/site-packages/textual/widgets/_ │
│ list_view.py:127 in action_cursor_down                                                           │
│                                                                                                  │
│   124 │   │   self.emit_no_wait(self.Selected(self, selected_child))                             │
│   125 │                                                                                          │
│   126 │   def action_cursor_down(self) -> None:                                                  │
│ ❱ 127 │   │   self.index += 1                                                                    │
│   128 │                                                                                          │
│   129 │   def action_cursor_up(self) -> None:                                                    │
│   130 │   │   self.index -= 1                                                                    │
│                                                                                                  │
│ ╭──────────────────────────── locals ─────────────────────────────╮                              │
│ │ self = CustomListView(pseudo_classes={'focus', 'focus-within'}) │                              │
│ ╰─────────────────────────────────────────────────────────────────╯                              │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
TypeError: unsupported operand type(s) for +=: 'NoneType' and 'int'
```


Discussion:
The quickest of fixes, which I'd like to confirm with @darrenburns first, is this:

```diff
diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py
index 062986cb..1ea7100d 100644
--- a/src/textual/widgets/_list_view.py
+++ b/src/textual/widgets/_list_view.py
@@ -52,6 +52,9 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
         super().__init__(*children, name=name, id=id, classes=classes)
         self.index = initial_index

+    def on_mount( self ) -> None:
+        self.index = self.index or 0
+
     @property
     def highlighted_child(self) -> ListItem | None:
         """ListItem | None: The currently highlighted ListItem,
```

Resolving patch:
diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py
--- a/src/textual/widgets/_list_view.py
+++ b/src/textual/widgets/_list_view.py
@@ -50,7 +50,11 @@ def __init__(
             classes: The CSS classes of the widget.
         """
         super().__init__(*children, name=name, id=id, classes=classes)
-        self.index = initial_index
+        self._index = initial_index
+
+    def on_mount(self) -> None:
+        """Ensure the ListView is fully-settled after mounting."""
+        self.index = self._index
 
     @property
     def highlighted_child(self) -> ListItem | None:


Test patch:
diff --git a/tests/listview/test_inherit_listview.py b/tests/listview/test_inherit_listview.py
new file mode 100644
--- /dev/null
+++ b/tests/listview/test_inherit_listview.py
@@ -0,0 +1,45 @@
+from textual.app import App, ComposeResult
+from textual.widgets import ListView, ListItem, Label
+
+
+class MyListView(ListView):
+    """Test child class of a ListView."""
+
+    def __init__(self, items: int = 0) -> None:
+        super().__init__()
+        self._items = items
+
+    def compose(self) -> ComposeResult:
+        """Compose the child widgets."""
+        for n in range(self._items):
+            yield ListItem(Label(f"This is item {n}"))
+
+
+class ListViewApp(App[None]):
+    """ListView test app."""
+
+    def __init__(self, items: int = 0) -> None:
+        super().__init__()
+        self._items = items
+
+    def compose(self) -> ComposeResult:
+        """Compose the child widgets."""
+        yield MyListView(self._items)
+
+
+async def test_empty_inherited_list_view() -> None:
+    """An empty self-populating inherited ListView should work as expected."""
+    async with ListViewApp().run_test() as pilot:
+        await pilot.press("tab")
+        assert pilot.app.query_one(MyListView).index is None
+        await pilot.press("down")
+        assert pilot.app.query_one(MyListView).index is None
+
+
+async def test_populated_inherited_list_view() -> None:
+    """A self-populating inherited ListView should work as normal."""
+    async with ListViewApp(30).run_test() as pilot:
+        await pilot.press("tab")
+        assert pilot.app.query_one(MyListView).index == 0
+        await pilot.press("down")
+        assert pilot.app.query_one(MyListView).index == 1