44
55from typing_extensions import TypeGuard
66
7- from textual . await_remove import AwaitRemove
8- from textual . binding import Binding , BindingType
9- from textual . containers import VerticalScroll
10- from textual . events import Mount
11- from textual . geometry import clamp
12- from textual .message import Message
13- from textual .reactive import reactive
14- from textual . widget import AwaitMount , Widget
15- from textual .widgets ._list_item import ListItem
7+ from .. import _widget_navigation
8+ from .. await_remove import AwaitRemove
9+ from .. binding import Binding , BindingType
10+ from .. containers import VerticalScroll
11+ from .. events import Mount
12+ from . .message import Message
13+ from . .reactive import reactive
14+ from .. widget import AwaitMount
15+ from . .widgets ._list_item import ListItem
1616
1717
1818class ListView (VerticalScroll , can_focus = True , can_focus_children = False ):
@@ -38,7 +38,7 @@ class ListView(VerticalScroll, can_focus=True, can_focus_children=False):
3838 | down | Move the cursor down. |
3939 """
4040
41- index = reactive [Optional [int ]](0 , always_update = True )
41+ index = reactive [Optional [int ]](0 , always_update = True , init = False )
4242 """The index of the currently highlighted item."""
4343
4444 class Highlighted (Message ):
@@ -117,7 +117,12 @@ def __init__(
117117 super ().__init__ (
118118 * children , name = name , id = id , classes = classes , disabled = disabled
119119 )
120- self ._index = initial_index
120+ # Set the index to the given initial index, or the first available index after.
121+ self ._index = _widget_navigation .find_next_enabled (
122+ self ._nodes ,
123+ anchor = initial_index - 1 if initial_index is not None else None ,
124+ direction = 1 ,
125+ )
121126
122127 def _on_mount (self , _ : Mount ) -> None :
123128 """Ensure the ListView is fully-settled after mounting."""
@@ -142,17 +147,17 @@ def validate_index(self, index: int | None) -> int | None:
142147 Returns:
143148 The clamped index.
144149 """
145- if not self . _nodes or index is None :
150+ if index is None or not self . _nodes :
146151 return None
147- return self ._clamp_index (index )
152+ elif index < 0 :
153+ return 0
154+ elif index >= len (self ._nodes ):
155+ return len (self ._nodes ) - 1
148156
149- def _clamp_index (self , index : int ) -> int :
150- """Clamp the index to a valid value given the current list of children"""
151- last_index = max (len (self ._nodes ) - 1 , 0 )
152- return clamp (index , 0 , last_index )
157+ return index
153158
154159 def _is_valid_index (self , index : int | None ) -> TypeGuard [int ]:
155- """Return True if the current index is valid given the current list of children"""
160+ """Determine whether the current index is valid into the list of children. """
156161 if index is None :
157162 return False
158163 return 0 <= index < len (self ._nodes )
@@ -164,16 +169,14 @@ def watch_index(self, old_index: int | None, new_index: int | None) -> None:
164169 assert isinstance (old_child , ListItem )
165170 old_child .highlighted = False
166171
167- new_child : Widget | None
168- if self ._is_valid_index (new_index ):
172+ if self ._is_valid_index (new_index ) and not self ._nodes [new_index ].disabled :
169173 new_child = self ._nodes [new_index ]
170174 assert isinstance (new_child , ListItem )
171175 new_child .highlighted = True
176+ self ._scroll_highlighted_region ()
177+ self .post_message (self .Highlighted (self , new_child ))
172178 else :
173- new_child = None
174-
175- self ._scroll_highlighted_region ()
176- self .post_message (self .Highlighted (self , new_child ))
179+ self .post_message (self .Highlighted (self , None ))
177180
178181 def extend (self , items : Iterable [ListItem ]) -> AwaitMount :
179182 """Append multiple new ListItems to the end of the ListView.
@@ -222,19 +225,30 @@ def action_select_cursor(self) -> None:
222225
223226 def action_cursor_down (self ) -> None :
224227 """Highlight the next item in the list."""
225- if self .index is None :
226- self .index = 0
227- return
228- self .index += 1
228+ candidate = _widget_navigation .find_next_enabled (
229+ self ._nodes ,
230+ anchor = self .index ,
231+ direction = 1 ,
232+ )
233+ if self .index is not None and candidate is not None and candidate < self .index :
234+ return # Avoid wrapping around.
235+
236+ self .index = candidate
229237
230238 def action_cursor_up (self ) -> None :
231239 """Highlight the previous item in the list."""
232- if self .index is None :
233- self .index = 0
234- return
235- self .index -= 1
240+ candidate = _widget_navigation .find_next_enabled (
241+ self ._nodes ,
242+ anchor = self .index ,
243+ direction = - 1 ,
244+ )
245+ if self .index is not None and candidate is not None and candidate > self .index :
246+ return # Avoid wrapping around.
247+
248+ self .index = candidate
236249
237250 def _on_list_item__child_clicked (self , event : ListItem ._ChildClicked ) -> None :
251+ event .stop ()
238252 self .focus ()
239253 self .index = self ._nodes .index (event .item )
240254 self .post_message (self .Selected (self , event .item ))
@@ -244,5 +258,6 @@ def _scroll_highlighted_region(self) -> None:
244258 if self .highlighted_child is not None :
245259 self .scroll_to_widget (self .highlighted_child , animate = False )
246260
247- def __len__ (self ):
261+ def __len__ (self ) -> int :
262+ """Compute the length (in number of items) of the list view."""
248263 return len (self ._nodes )
0 commit comments