diff --git a/src/core/classes/tabs.ts b/src/core/classes/tabs.ts index dbec45e01..bccb13a6a 100644 --- a/src/core/classes/tabs.ts +++ b/src/core/classes/tabs.ts @@ -22,7 +22,6 @@ import { OnDestroy, AfterViewInit, ViewChild, - ElementRef, SimpleChange, } from '@angular/core'; import { IonSlides } from '@ionic/angular'; @@ -34,6 +33,8 @@ import { CoreSettingsHelper } from '@features/settings/services/settings-helper' import { CoreAriaRoleTab, CoreAriaRoleTabFindable } from './aria-role-tab'; import { CoreEventObserver } from '@singletons/events'; import { CoreDom } from '@singletons/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreError } from './errors/error'; /** * Class to abstract some common code for tabs. @@ -54,6 +55,7 @@ export class CoreTabsBaseComponent implements OnInit, Aft tabs: T[] = []; // List of tabs. + hideTabs = false; selected?: string; // Selected tab id. showPrevButton = false; showNextButton = false; @@ -65,10 +67,11 @@ export class CoreTabsBaseComponent implements OnInit, Aft initialSlide: 0, slidesPerView: 3, centerInsufficientSlides: true, + threshold: 10, }; + protected slidesElement?: HTMLIonSlidesElement; protected initialized = false; - protected afterViewInitTriggered = false; protected resizeListener?: CoreEventObserver; protected isDestroyed = false; @@ -79,54 +82,41 @@ export class CoreTabsBaseComponent implements OnInit, Aft protected firstSelectedTab?: string; // ID of the first selected tab to control history. protected backButtonFunction: (event: BackButtonEvent) => void; - protected languageChangedSubscription?: Subscription; // Swiper 6 documentation: https://swiper6.vercel.app/ protected isInTransition = false; // Wether Slides is in transition. - protected slidesSwiper: any; // eslint-disable-line @typescript-eslint/no-explicit-any - protected slidesSwiperLoaded = false; + protected subscriptions: Subscription[] = []; tabAction: CoreTabsRoleTab; - constructor( - protected element: ElementRef, - ) { + constructor() { this.backButtonFunction = this.backButtonClicked.bind(this); this.tabAction = new CoreTabsRoleTab(this); } /** - * Component being initialized. + * @inheritdoc */ - ngOnInit(): void { + async ngOnInit(): Promise { this.direction = Platform.isRTL ? 'rtl' : 'ltr'; // Change the side when the language changes. - this.languageChangedSubscription = Translate.onLangChange.subscribe(() => { + this.subscriptions.push(Translate.onLangChange.subscribe(() => { setTimeout(() => { this.direction = Platform.isRTL ? 'rtl' : 'ltr'; }); - }); + })); } /** - * View has been initialized. + * @inheritdoc */ - async ngAfterViewInit(): Promise { + ngAfterViewInit(): void { if (this.isDestroyed) { return; } - this.afterViewInitTriggered = true; - - if (!this.initialized && this.hideUntil) { - // Tabs should be shown, initialize them. - await this.initializeTabs(); - } - - this.resizeListener = CoreDom.onWindowResize(() => { - this.windowResized(); - }); + this.init(); } /** @@ -134,14 +124,7 @@ export class CoreTabsBaseComponent implements OnInit, Aft */ // eslint-disable-next-line @typescript-eslint/no-unused-vars ngOnChanges(changes: Record): void { - // Wait for ngAfterViewInit so it works in the case that each tab has its own component. - if (!this.initialized && this.hideUntil && this.afterViewInitTriggered) { - // Tabs should be shown, initialize them. - // Use a setTimeout so child components update their inputs before initializing the tabs. - setTimeout(() => { - this.initializeTabs(); - }); - } + this.init(); } /** @@ -206,6 +189,16 @@ export class CoreTabsBaseComponent implements OnInit, Aft return; } + this.numTabsShown = this.tabs.reduce((prev: number, current) => current.enabled ? prev + 1 : prev, 0); + + if (this.numTabsShown <= 1) { + this.hideTabs = true; + + // Only one, nothing to do here. + return; + } + this.hideTabs = false; + await this.calculateMaxSlides(); await this.updateSlides(); @@ -233,28 +226,81 @@ export class CoreTabsBaseComponent implements OnInit, Aft } /** - * Initialize the tabs, determining the first tab to be shown. + * Init the component. */ - protected async initializeTabs(): Promise { - // Initialize slider. - this.slidesSwiper = await this.slides?.getSwiper(); - this.slidesSwiper.once('progress', () => { - this.slidesSwiperLoaded = true; - this.calculateSlides(); - }); - - const selectedTab = this.calculateInitialTab(); - if (!selectedTab) { + protected async init(): Promise { + if (!this.hideUntil) { + // Hidden, do nothing. return; } - this.firstSelectedTab = selectedTab.id!; - this.selectTab(this.firstSelectedTab); + try { + await this.initializeSlider(); + await this.initializeTabs(); + } catch { + // Something went wrong, ignore. + } + } + + /** + * Initialize the slider elements. + */ + protected async initializeSlider(): Promise { + if (this.initialized) { + return; + } + + if (this.slidesElement) { + // Already initializated, await for ready. + await this.slidesElement.componentOnReady(); + + return; + } + + if (!this.slides) { + await CoreUtils.nextTick(); + } + const slidesSwiper = await this.slides?.getSwiper(); + if (!slidesSwiper || !this.slides) { + throw new CoreError('Swiper not found, will try on next change.'); + } + + this.slidesElement = slidesSwiper.el; + await this.slidesElement.componentOnReady(); this.initialized = true; + // Subscribe to changes. + this.subscriptions.push(this.slides.ionSlideDidChange.subscribe(() => { + this.slideChanged(); + })); + } + + /** + * Initialize the tabs, determining the first tab to be shown. + */ + protected async initializeTabs(): Promise { + if (!this.initialized || !this.slidesElement) { + return; + } + + const selectedTab = this.calculateInitialTab(); + if (!selectedTab) { + // No enabled tabs, return. + throw new CoreError('No enabled tabs.'); + } + + this.firstSelectedTab = selectedTab.id; + if (this.firstSelectedTab !== undefined) { + this.selectTab(this.firstSelectedTab); + } + // Check which arrows should be shown. this.calculateSlides(); + + this.resizeListener = CoreDom.onWindowResize(() => { + this.calculateSlides(); + }); } /** @@ -277,7 +323,7 @@ export class CoreTabsBaseComponent implements OnInit, Aft * Method executed when the slides are changed. */ async slideChanged(): Promise { - if (!this.slidesSwiperLoaded) { + if (!this.slidesElement) { return; } @@ -302,17 +348,15 @@ export class CoreTabsBaseComponent implements OnInit, Aft * Updates the number of slides to show. */ protected async updateSlides(): Promise { - this.numTabsShown = this.tabs.reduce((prev: number, current) => current.enabled ? prev + 1 : prev, 0); + if (!this.slides) { + return; + } this.slidesOpts = { ...this.slidesOpts, slidesPerView: Math.min(this.maxSlides, this.numTabsShown) }; - this.slideChanged(); + await this.slideChanged(); - // @todo: This call to update() can trigger JS errors in the console if tabs are re-loaded and there's only 1 tab. - // For some reason, swiper.slides is undefined inside the Slides class, and the swiper is marked as destroyed. - // Changing *ngIf="hideUntil" to [hidden] doesn't solve the issue, and it causes another error to be raised. - // This can be tested in lesson as a student, play a lesson and go back to the entry page. - await this.slides!.update(); + await this.slides.update(); if (!this.hasSliddenToInitial && this.selectedIndex && this.selectedIndex >= this.slidesOpts.slidesPerView) { this.hasSliddenToInitial = true; @@ -320,7 +364,7 @@ export class CoreTabsBaseComponent implements OnInit, Aft setTimeout(() => { if (this.shouldSlideToInitial) { - this.slides!.slideTo(this.selectedIndex, 0); + this.slides?.slideTo(this.selectedIndex, 0); this.shouldSlideToInitial = false; } }, 400); @@ -339,17 +383,23 @@ export class CoreTabsBaseComponent implements OnInit, Aft * Calculate the number of slides that can fit on the screen. */ protected async calculateMaxSlides(): Promise { - if (!this.slidesSwiperLoaded) { + if (!this.slidesElement || !this.slides) { return; } this.maxSlides = 3; - let width = this.slidesSwiper.width; - if (!width) { - this.slidesSwiper.updateSize(); - width = this.slidesSwiper.width; + await CoreUtils.nextTick(); + let width: number = this.slidesElement.getBoundingClientRect().width; + if (!width) { + const slidesSwiper = await this.slides.getSwiper(); + + await slidesSwiper.updateSize(); + await CoreUtils.nextTick(); + + width = slidesSwiper.width; if (!width) { + return; } } @@ -456,12 +506,12 @@ export class CoreTabsBaseComponent implements OnInit, Aft return; } - if (this.selected) { + if (this.selected && this.slides) { // Check if we need to slide to the tab because it's not visible. - const firstVisibleTab = await this.slides!.getActiveIndex(); + const firstVisibleTab = await this.slides.getActiveIndex(); const lastVisibleTab = firstVisibleTab + this.slidesOpts.slidesPerView - 1; if (index < firstVisibleTab || index > lastVisibleTab) { - await this.slides!.slideTo(index, 0, true); + await this.slides.slideTo(index, 0, true); } } @@ -470,9 +520,9 @@ export class CoreTabsBaseComponent implements OnInit, Aft return; } - const ok = await this.loadTab(tabToSelect); + const suceeded = await this.loadTab(tabToSelect); - if (ok !== false) { + if (suceeded !== false) { this.tabSelected(tabToSelect, index); } } @@ -503,15 +553,6 @@ export class CoreTabsBaseComponent implements OnInit, Aft return true; } - /** - * Adapt tabs to a window resize. - */ - protected windowResized(): void { - setTimeout(() => { - this.calculateSlides(); - }, 200); - } - /** * Component destroyed. */ @@ -519,7 +560,7 @@ export class CoreTabsBaseComponent implements OnInit, Aft this.isDestroyed = true; this.resizeListener?.off(); - this.languageChangedSubscription?.unsubscribe(); + this.subscriptions.forEach((subscription) => subscription.unsubscribe()); } } @@ -541,8 +582,8 @@ class CoreTabsRoleTab extends CoreAriaRoleTab tab.enabled).map((tab) => ({ - id: tab.id!, - findIndex: tab.id!, + id: tab.id || '', + findIndex: tab.id || '', })); } diff --git a/src/core/components/tabs-outlet/core-tabs-outlet.html b/src/core/components/tabs-outlet/core-tabs-outlet.html index 7ef200d94..7d0b9e925 100644 --- a/src/core/components/tabs-outlet/core-tabs-outlet.html +++ b/src/core/components/tabs-outlet/core-tabs-outlet.html @@ -6,10 +6,9 @@ - + - - + - = new EventEmitter(); @ContentChild(TemplateRef) template?: TemplateRef; // Template defined by the content. @@ -82,7 +82,7 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase { element: ElementRef, ) { this.element = element.nativeElement; - + this.id = this.id || 'core-tab-' + CoreUtils.getUniqueId('CoreTabComponent'); this.element.setAttribute('role', 'tabpanel'); this.element.setAttribute('tabindex', '0'); this.element.setAttribute('aria-hidden', 'true'); @@ -92,7 +92,6 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase { * Component being initialized. */ ngOnInit(): void { - this.id = this.id || 'core-tab-' + CoreUtils.getUniqueId('CoreTabComponent'); this.element.setAttribute('aria-labelledby', this.id + '-tab'); this.element.setAttribute('id', this.id); diff --git a/src/core/components/tabs/tabs.scss b/src/core/components/tabs/tabs.scss index 04a50ba1b..8aa6e50c8 100644 --- a/src/core/components/tabs/tabs.scss +++ b/src/core/components/tabs/tabs.scss @@ -39,7 +39,7 @@ ion-tab-button { max-width: 100%; ion-label { - font-size: 16px; + font-size: 14px; font-weight: 400; text-overflow: ellipsis; white-space: nowrap; @@ -109,8 +109,3 @@ position: relative; } } - - -:host-context(.ios) { - --height: 53px; -} diff --git a/src/core/components/tabs/tabs.ts b/src/core/components/tabs/tabs.ts index e540dab55..a603c3ef8 100644 --- a/src/core/components/tabs/tabs.ts +++ b/src/core/components/tabs/tabs.ts @@ -51,12 +51,6 @@ export class CoreTabsComponent extends CoreTabsBaseComponent i protected originalTabsContainer?: HTMLElement; // The container of the original tabs. It will include each tab's content. - constructor( - element: ElementRef, - ) { - super(element); - } - /** * View has been initialized. */ @@ -84,7 +78,7 @@ export class CoreTabsComponent extends CoreTabsBaseComponent i */ addTab(tab: CoreTabComponent): void { // Check if tab is already in the list. - if (this.getTabIndex(tab.id!) == -1) { + if (this.getTabIndex(tab.id) === -1) { this.tabs.push(tab); this.sortTabs(); @@ -100,7 +94,7 @@ export class CoreTabsComponent extends CoreTabsBaseComponent i * @param tab The tab to remove. */ removeTab(tab: CoreTabComponent): void { - const index = this.getTabIndex(tab.id!); + const index = this.getTabIndex(tab.id); this.tabs.splice(index, 1); this.calculateSlides(); diff --git a/src/core/features/course/pages/index/index.ts b/src/core/features/course/pages/index/index.ts index fdd5e47c8..462a97c56 100644 --- a/src/core/features/course/pages/index/index.ts +++ b/src/core/features/course/pages/index/index.ts @@ -131,6 +131,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { } catch (error) { CoreDomUtils.showErrorModal(error); CoreNavigator.back(); + this.loaded = true; return; } @@ -224,9 +225,9 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { // Select the tab if needed. this.firstTabName = undefined; if (tabToLoad) { - setTimeout(() => { - this.tabsComponent?.selectByIndex(tabToLoad!); - }); + await CoreUtils.nextTick(); + + this.tabsComponent?.selectByIndex(tabToLoad); } }