MOBILE-3926 core: Fix swipe navigation
This commit is contained in:
		
							parent
							
								
									04d2bcfe85
								
							
						
					
					
						commit
						f668f874a5
					
				| @ -39,21 +39,21 @@ export class CoreSwipeNavigationItemsManager< | ||||
|      * Navigate to the next item. | ||||
|      */ | ||||
|     async navigateToNextItem(): Promise<void> { | ||||
|         await this.navigateToItemBy(-1, 'back'); | ||||
|         await this.navigateToItemBy(1, 'forward'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Navigate to the previous item. | ||||
|      */ | ||||
|     async navigateToPreviousItem(): Promise<void> { | ||||
|         await this.navigateToItemBy(1, 'forward'); | ||||
|         await this.navigateToItemBy(-1, 'back'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Has a next item. | ||||
|      */ | ||||
|     async hasNextItem(): Promise<boolean> { | ||||
|         const item = await this.getItemBy(-1); | ||||
|         const item = await this.getItemBy(1); | ||||
| 
 | ||||
|         return !!item; | ||||
|     } | ||||
| @ -62,7 +62,7 @@ export class CoreSwipeNavigationItemsManager< | ||||
|      * Has a previous item. | ||||
|      */ | ||||
|     async hasPreviousItem(): Promise<boolean> { | ||||
|         const item = await this.getItemBy(1); | ||||
|         const item = await this.getItemBy(-1); | ||||
| 
 | ||||
|         return !!item; | ||||
|     } | ||||
| @ -100,13 +100,7 @@ export class CoreSwipeNavigationItemsManager< | ||||
|      * @param animationDirection Animation direction. | ||||
|      */ | ||||
|     protected async navigateToItemBy(delta: number, animationDirection: 'forward' | 'back'): Promise<void> { | ||||
|         let item: Item | null; | ||||
| 
 | ||||
|         do { | ||||
|             item = await this.getItemBy(delta); | ||||
| 
 | ||||
|             delta += delta > 0 ? 1 : -1; | ||||
|         } while (item && this.skipItemInSwipe(item)); | ||||
|         const item = await this.getItemBy(delta); | ||||
| 
 | ||||
|         if (!item) { | ||||
|             return; | ||||
| @ -122,25 +116,41 @@ export class CoreSwipeNavigationItemsManager< | ||||
|      */ | ||||
|     protected async getItemBy(delta: number): Promise<Item | null> { | ||||
|         const items = this.getSource().getItems(); | ||||
| 
 | ||||
|         // Get selected item.
 | ||||
|         const selectedIndex = (this.selectedItem && items?.indexOf(this.selectedItem)) ?? -1; | ||||
|         const nextIndex = selectedIndex + delta; | ||||
| 
 | ||||
|         if (selectedIndex === -1 || nextIndex < 0) { | ||||
|         if (selectedIndex === -1 || items === null) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         // Get item by delta.
 | ||||
|         const item = items?.[nextIndex] ?? null; | ||||
|         const deltaStep = delta > 0 ? 1 : -1; | ||||
|         let nextIndex = selectedIndex; | ||||
|         let deltaMoved = 0; | ||||
| 
 | ||||
|         if (!item && !this.getSource().isCompleted()) { | ||||
|         while (deltaMoved !== delta) { | ||||
|             nextIndex += deltaStep; | ||||
| 
 | ||||
|             if (nextIndex < 0 || nextIndex >= items.length) { | ||||
|                 break; | ||||
|             } | ||||
| 
 | ||||
|             if (this.skipItemInSwipe(items[nextIndex])) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             deltaMoved += deltaStep; | ||||
|         } | ||||
| 
 | ||||
|         if (deltaMoved === delta) { | ||||
|             return items[nextIndex]; | ||||
|         } | ||||
| 
 | ||||
|         if (!this.getSource().isCompleted()) { | ||||
|             await this.getSource().load(); | ||||
| 
 | ||||
|             return this.getItemBy(delta); | ||||
|         } | ||||
| 
 | ||||
|         return item; | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
							
								
								
									
										135
									
								
								src/core/classes/tests/swipe-navigation-items-manager.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/core/classes/tests/swipe-navigation-items-manager.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,135 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { mock, mockSingleton } from '@/testing/utils'; | ||||
| import { ActivatedRoute, ActivatedRouteSnapshot, UrlSegment } from '@angular/router'; | ||||
| import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source'; | ||||
| import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| 
 | ||||
| interface Item { | ||||
|     path: string; | ||||
| } | ||||
| 
 | ||||
| class StubSource extends CoreRoutedItemsManagerSource<Item> { | ||||
| 
 | ||||
|     stubItems: Item[]; | ||||
| 
 | ||||
|     constructor(stubItems: Item[] = []) { | ||||
|         super(); | ||||
| 
 | ||||
|         this.stubItems = stubItems; | ||||
|     } | ||||
| 
 | ||||
|     getItemPath(item: Item): string { | ||||
|         return item.path; | ||||
|     } | ||||
| 
 | ||||
|     protected async loadPageItems(): Promise<{ items: Item[] }> { | ||||
|         return { items: this.stubItems }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| class StubManager extends CoreSwipeNavigationItemsManager { | ||||
| 
 | ||||
|     skipItemInSwipe(item: Item): boolean { | ||||
|         return item.path.includes('skip'); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| describe('CoreSwipeNavigationItemsManager', () => { | ||||
| 
 | ||||
|     let items: Item[]; | ||||
|     let currentPath: string; | ||||
|     let source: StubSource; | ||||
|     let instance: StubManager; | ||||
| 
 | ||||
|     beforeEach(async () => { | ||||
|         mockSingleton(CoreNavigator, { | ||||
|             navigate: jest.fn(), | ||||
|             getCurrentRoute: () => mock<ActivatedRoute>({ | ||||
|                 snapshot: mock<ActivatedRouteSnapshot>({ | ||||
|                     url: [mock<UrlSegment>({ path: currentPath })], | ||||
|                 }), | ||||
|             }), | ||||
|         }); | ||||
| 
 | ||||
|         items = []; | ||||
|         currentPath = ''; | ||||
|         source = new StubSource(items); | ||||
|         instance = new StubManager(source); | ||||
|     }); | ||||
| 
 | ||||
|     it('navigates to next item', async () => { | ||||
|         // Arrange.
 | ||||
|         currentPath = 'foo'; | ||||
|         items.push({ path: 'foo' }); | ||||
|         items.push({ path: 'bar' }); | ||||
| 
 | ||||
|         await source.load(); | ||||
| 
 | ||||
|         // Act.
 | ||||
|         await instance.navigateToNextItem(); | ||||
| 
 | ||||
|         // Assert.
 | ||||
|         expect(CoreNavigator.navigate).toHaveBeenCalledWith('../bar', { animationDirection: 'forward', params: {}, replace: true }); | ||||
|     }); | ||||
| 
 | ||||
|     it('navigates to previous item', async () => { | ||||
|         // Arrange.
 | ||||
|         currentPath = 'bar'; | ||||
|         items.push({ path: 'foo' }); | ||||
|         items.push({ path: 'bar' }); | ||||
| 
 | ||||
|         await source.load(); | ||||
| 
 | ||||
|         // Act.
 | ||||
|         await instance.navigateToPreviousItem(); | ||||
| 
 | ||||
|         // Assert.
 | ||||
|         expect(CoreNavigator.navigate).toHaveBeenCalledWith('../foo', { animationDirection: 'back', params: {}, replace: true }); | ||||
|     }); | ||||
| 
 | ||||
|     it('skips items', async () => { | ||||
|         // Arrange.
 | ||||
|         currentPath = 'foo'; | ||||
|         items.push({ path: 'foo' }); | ||||
|         items.push({ path: 'skip' }); | ||||
|         items.push({ path: 'bar' }); | ||||
| 
 | ||||
|         await source.load(); | ||||
| 
 | ||||
|         // Act.
 | ||||
|         await instance.navigateToNextItem(); | ||||
| 
 | ||||
|         // Assert.
 | ||||
|         expect(CoreNavigator.navigate).toHaveBeenCalledWith('../bar', { animationDirection: 'forward', params: {}, replace: true }); | ||||
|     }); | ||||
| 
 | ||||
|     it('checks items', async () => { | ||||
|         // Arrange.
 | ||||
|         currentPath = 'foo'; | ||||
|         items.push({ path: 'foo' }); | ||||
|         items.push({ path: 'bar' }); | ||||
| 
 | ||||
|         await source.load(); | ||||
| 
 | ||||
|         // Assert.
 | ||||
|         await expect(instance.hasNextItem()).resolves.toBe(true); | ||||
|         await expect(instance.hasPreviousItem()).resolves.toBe(false); | ||||
|     }); | ||||
| 
 | ||||
| }); | ||||
| @ -14,7 +14,7 @@ | ||||
| 
 | ||||
| import {  AfterViewInit, Directive, ElementRef, Input, OnDestroy } from '@angular/core'; | ||||
| import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; | ||||
| import { Gesture } from '@ionic/angular'; | ||||
| import { Gesture, GestureDetail } from '@ionic/angular'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { GestureController } from '@singletons'; | ||||
| 
 | ||||
| @ -65,45 +65,7 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy { | ||||
|                 style.transform = `translateX(${ev.deltaX * SWIPE_FRICTION }px)`; | ||||
|             }, | ||||
|             onEnd: (ev) => { | ||||
|                 style.transition = '.5s ease-out'; | ||||
| 
 | ||||
|                 if (ev.deltaX > ACTIVATION_THRESHOLD) { | ||||
|                     this.manager?.hasNextItem().then((hasNext) => { | ||||
|                         if (hasNext) { | ||||
|                             this.preventClickOnElement(); | ||||
| 
 | ||||
|                             style.transform = 'translateX(100%) !important'; | ||||
|                             this.swipeRight(); | ||||
|                         } else { | ||||
|                             style.transform = ''; | ||||
|                         } | ||||
| 
 | ||||
|                         return; | ||||
|                     }); | ||||
| 
 | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 if (ev.deltaX < -ACTIVATION_THRESHOLD) { | ||||
|                     this.manager?.hasPreviousItem().then((hasPrevious) => { | ||||
|                         if (hasPrevious) { | ||||
| 
 | ||||
|                             this.preventClickOnElement(); | ||||
| 
 | ||||
|                             style.transform = 'translateX(-100%) !important'; | ||||
|                             this.swipeLeft(); | ||||
|                         } else { | ||||
|                             style.transform = ''; | ||||
|                         } | ||||
| 
 | ||||
|                         return; | ||||
|                     }); | ||||
| 
 | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 style.transform = ''; | ||||
| 
 | ||||
|                 this.onRelease(ev); | ||||
|             }, | ||||
|         }); | ||||
|         this.swipeGesture.enable(); | ||||
| @ -117,7 +79,7 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.manager?.navigateToPreviousItem(); | ||||
|         this.manager?.navigateToNextItem(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -128,7 +90,7 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.manager?.navigateToNextItem(); | ||||
|         this.manager?.navigateToPreviousItem(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -155,4 +117,33 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy { | ||||
|         this.swipeGesture?.destroy(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handle swipe release event. | ||||
|      * | ||||
|      * @param event Event. | ||||
|      */ | ||||
|     protected async onRelease(event: GestureDetail): Promise<void> { | ||||
|         this.element.style.transition = '.5s ease-out'; | ||||
| 
 | ||||
|         if (event.deltaX > ACTIVATION_THRESHOLD && await this.manager?.hasPreviousItem()) { | ||||
|             this.preventClickOnElement(); | ||||
|             this.swipeRight(); | ||||
| 
 | ||||
|             this.element.style.transform = 'translateX(100%) !important'; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (event.deltaX < -ACTIVATION_THRESHOLD && await this.manager?.hasNextItem()) { | ||||
|             this.element.style.transform = 'translateX(-100%) !important'; | ||||
| 
 | ||||
|             this.preventClickOnElement(); | ||||
|             this.swipeLeft(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.element.style.transform = ''; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user