@@ -48,6 +48,13 @@ export function VersioningSidebarList() {
4848 const listRef = useRef < HTMLDivElement > ( null ) ;
4949 const paginationRef = useRef < HTMLDivElement > ( null ) ;
5050 const [ activeIndex , setActiveIndex ] = useState ( 0 ) ;
51+ const pendingNavigation = useRef <
52+ | {
53+ after : string ;
54+ namedOnly : boolean ;
55+ }
56+ | undefined
57+ > ( undefined ) ;
5158 const nextCursor = state . mode === "versions" ? state . nextCursor : undefined ;
5259
5360 const focusRow = useCallback ( ( index : number ) => {
@@ -69,6 +76,73 @@ export function VersioningSidebarList() {
6976 [ list , namedOnly ] ,
7077 ) ;
7178
79+ const needsOlderPage = useCallback (
80+ ( index : number ) =>
81+ nextCursor !== undefined &&
82+ ( ! rows [ index ] ||
83+ ( versioning . historyIncludesBeginning &&
84+ rows [ index ] ! . snapshot . id ===
85+ ( list . loaded ? list . snapshots . at ( - 1 ) ?. id : undefined ) ) ) ,
86+ [ nextCursor , rows , versioning . historyIncludesBeginning , list ] ,
87+ ) ;
88+
89+ async function advanceAfterPage ( after : string ) {
90+ const navigation = { after, namedOnly } ;
91+ pendingNavigation . current = navigation ;
92+ try {
93+ return await run ( async ( ) => {
94+ while ( pendingNavigation . current === navigation ) {
95+ const outcome = await versioning . loadMore ( ) ;
96+ if ( outcome . status !== "done" ) {
97+ return outcome ;
98+ }
99+ // Let the newly loaded rows mount before moving native focus.
100+ await new Promise < void > ( ( resolve ) => {
101+ requestAnimationFrame ( ( ) => resolve ( ) ) ;
102+ } ) ;
103+ if (
104+ pendingNavigation . current !== navigation ||
105+ focusedRowId . current !== after
106+ ) {
107+ return { status : "cancelled" } ;
108+ }
109+ const latest = versioning . store . state ;
110+ const latestList = getVersionList ( latest ) ;
111+ if ( latest . mode !== "versions" || ! latestList . loaded ) {
112+ return { status : "cancelled" } ;
113+ }
114+ const latestRows = getVisibleVersionRows ( latestList , namedOnly ) ;
115+ const index = latestRows . findIndex (
116+ ( row ) => row . snapshot . id === after ,
117+ ) ;
118+ if ( index === - 1 ) {
119+ return { status : "cancelled" } ;
120+ }
121+ const next = latestRows [ index + 1 ] ;
122+ if (
123+ latest . nextCursor !== undefined &&
124+ ( ! next ||
125+ ( versioning . historyIncludesBeginning &&
126+ next . snapshot . id === latestList . snapshots . at ( - 1 ) ?. id ) )
127+ ) {
128+ continue ;
129+ }
130+ if ( ! next ) {
131+ return { status : "done" } ;
132+ }
133+ pendingNavigation . current = undefined ;
134+ focusRow ( index + 1 ) ;
135+ return previewRow ( next . snapshot ) ;
136+ }
137+ return { status : "cancelled" } ;
138+ } ) ;
139+ } finally {
140+ if ( pendingNavigation . current === navigation ) {
141+ pendingNavigation . current = undefined ;
142+ }
143+ }
144+ }
145+
72146 useEffect ( ( ) => {
73147 if ( ! list . loaded || listing || listError || nextCursor === undefined ) {
74148 return ;
@@ -160,7 +234,15 @@ export function VersioningSidebarList() {
160234 return ;
161235 }
162236
163- function scrubTo ( destination : number ) {
237+ function scrubTo ( destination : number , waitForPage = false ) {
238+ if ( waitForPage && pendingNavigation . current ) {
239+ return ;
240+ }
241+ pendingNavigation . current = undefined ;
242+ if ( waitForPage && needsOlderPage ( destination ) ) {
243+ void advanceAfterPage ( rows [ index ] ! . snapshot . id ) ;
244+ return ;
245+ }
164246 const clamped = Math . max ( 0 , Math . min ( destination , rows . length - 1 ) ) ;
165247 if ( event . repeat && clamped === index ) {
166248 return ;
@@ -175,7 +257,7 @@ export function VersioningSidebarList() {
175257 switch ( event . key ) {
176258 case "ArrowDown" :
177259 event . preventDefault ( ) ;
178- scrubTo ( index + 1 ) ;
260+ scrubTo ( index + 1 , true ) ;
179261 break ;
180262 case "ArrowUp" :
181263 event . preventDefault ( ) ;
@@ -192,6 +274,7 @@ export function VersioningSidebarList() {
192274 case "Enter" :
193275 case " " :
194276 event . preventDefault ( ) ;
277+ pendingNavigation . current = undefined ;
195278 void run ( ( ) => previewRow ( rows [ index ] ! . snapshot ) ) ;
196279 break ;
197280 default :
@@ -207,6 +290,9 @@ export function VersioningSidebarList() {
207290 aria-label = { dict . versioning . versions_list }
208291 aria-busy = { listing || undefined }
209292 ref = { listRef }
293+ onPointerDown = { ( ) => {
294+ pendingNavigation . current = undefined ;
295+ } }
210296 onBlur = { ( event ) => {
211297 // Registered editor menus can live outside the list's DOM.
212298 const target = event . relatedTarget ;
@@ -216,6 +302,7 @@ export function VersioningSidebarList() {
216302 target . closest ( '[role="menu"]' ) ;
217303 if ( ! event . currentTarget . contains ( target ) && ! inEditorMenu ) {
218304 focusedRowId . current = undefined ;
305+ pendingNavigation . current = undefined ;
219306 }
220307 } }
221308 >
@@ -234,6 +321,7 @@ export function VersioningSidebarList() {
234321 }
235322 onKeyDown = { ( event ) => handleKeyDown ( event , index ) }
236323 onFocus = { ( ) => {
324+ pendingNavigation . current = undefined ;
237325 focusedRowId . current = snapshot . id ;
238326 setActiveIndex ( index ) ;
239327 } }
0 commit comments