diff --git a/src/addons/mod/quiz/components/preflight-modal/preflight-modal.ts b/src/addons/mod/quiz/components/preflight-modal/preflight-modal.ts index f15da32c7..ae2632ced 100644 --- a/src/addons/mod/quiz/components/preflight-modal/preflight-modal.ts +++ b/src/addons/mod/quiz/components/preflight-modal/preflight-modal.ts @@ -47,6 +47,7 @@ export class AddonModQuizPreflightModalComponent implements OnInit { constructor( formBuilder: FormBuilder, + protected elementRef: ElementRef, ) { // Create an empty form group. The controls will be added by the access rules components. this.preflightForm = formBuilder.group({}); @@ -115,7 +116,12 @@ export class AddonModQuizPreflightModalComponent implements OnInit { if (!this.preflightForm.valid) { // Form not valid. Scroll to the first element with errors. - if (!CoreDomUtils.instance.scrollToInputError(this.content)) { + const hasScrolled = CoreDomUtils.instance.scrollToInputError( + this.elementRef.nativeElement, + this.content, + ); + + if (!hasScrolled) { // Input not found, show an error modal. CoreDomUtils.instance.showErrorModal('core.errorinvalidform', true); } diff --git a/src/addons/mod/quiz/pages/player/player.ts b/src/addons/mod/quiz/pages/player/player.ts index f6124b557..046db21de 100644 --- a/src/addons/mod/quiz/pages/player/player.ts +++ b/src/addons/mod/quiz/pages/player/player.ts @@ -681,9 +681,11 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { * @param slot Slot of the question to scroll to. */ protected scrollToQuestion(slot: number): void { - if (this.content) { - CoreDomUtils.instance.scrollToElementBySelector(this.content, '#addon-mod_quiz-question-' + slot); - } + CoreDomUtils.instance.scrollToElementBySelector( + this.elementRef.nativeElement, + this.content, + '#addon-mod_quiz-question-' + slot, + ); } /** diff --git a/src/core/directives/link.ts b/src/core/directives/link.ts index 9d31dfdda..c5c7597d4 100644 --- a/src/core/directives/link.ts +++ b/src/core/directives/link.ts @@ -104,7 +104,11 @@ export class CoreLinkDirective implements OnInit { if (href.charAt(0) == '#') { // Look for id or name. href = href.substr(1); - CoreDomUtils.instance.scrollToElementBySelector(this.content, '#' + href + ', [name=\'' + href + '\']'); + CoreDomUtils.instance.scrollToElementBySelector( + this.element.closest('ion-content'), + this.content, + `#${href}, [name='${href}']`, + ); return; } diff --git a/src/core/features/course/components/format/format.ts b/src/core/features/course/components/format/format.ts index 060906f96..3ca97684b 100644 --- a/src/core/features/course/components/format/format.ts +++ b/src/core/features/course/components/format/format.ts @@ -25,6 +25,7 @@ import { QueryList, Type, ViewChild, + ElementRef, } from '@angular/core'; import { CoreSites } from '@services/sites'; @@ -111,6 +112,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { constructor( protected content: IonContent, + protected elementRef: ElementRef, ) { // Pass this instance to all components so they can use its methods and properties. this.data.coreCourseFormatComponent = this; @@ -402,7 +404,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { if (this.moduleId && typeof previousValue == 'undefined') { setTimeout(() => { - CoreDomUtils.instance.scrollToElementBySelector(this.content, '#core-course-module-' + this.moduleId); + CoreDomUtils.instance.scrollToElementBySelector( + this.elementRef.nativeElement, + this.content, + '#core-course-module-' + this.moduleId, + ); }, 200); } else { this.content.scrollToTop(0); diff --git a/src/core/features/login/pages/email-signup/email-signup.ts b/src/core/features/login/pages/email-signup/email-signup.ts index 67cdcf4eb..973862b9c 100644 --- a/src/core/features/login/pages/email-signup/email-signup.ts +++ b/src/core/features/login/pages/email-signup/email-signup.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ViewChild, ElementRef, OnInit } from '@angular/core'; +import { Component, ViewChild, ElementRef, OnInit, ChangeDetectorRef } from '@angular/core'; import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; import { IonContent, IonRefresher } from '@ionic/angular'; @@ -81,6 +81,8 @@ export class CoreLoginEmailSignupPage implements OnInit { constructor( protected fb: FormBuilder, + protected elementRef: ElementRef, + protected changeDetector: ChangeDetectorRef, ) { // Create the ageVerificationForm. this.ageVerificationForm = this.fb.group({ @@ -272,8 +274,17 @@ export class CoreLoginEmailSignupPage implements OnInit { e.stopPropagation(); if (!this.signupForm.valid || (this.settings?.recaptchapublickey && !this.captcha.recaptcharesponse)) { - // Form not valid. Scroll to the first element with errors. - const errorFound = await CoreDomUtils.instance.scrollToInputError(this.content); + // Form not valid. Mark all controls as dirty to display errors. + for (const name in this.signupForm.controls) { + this.signupForm.controls[name].markAsDirty(); + } + this.changeDetector.detectChanges(); + + // Scroll to the first element with errors. + const errorFound = CoreDomUtils.instance.scrollToInputError( + this.elementRef.nativeElement, + this.content, + ); if (!errorFound) { // Input not found, show an error modal. diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index f09b70c54..813078fde 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -1119,24 +1119,26 @@ export class CoreDomUtilsProvider { /** * Scroll to a certain element using a selector to find it. * + * @param container The element that contains the element that must be scrolled. * @param content The content that must be scrolled. * @param selector Selector to find the element to scroll to. * @param scrollParentClass Parent class where to stop calculating the position. Default inner-scroll. * @param duration Duration of the scroll animation in milliseconds. * @return True if the element is found, false otherwise. */ - async scrollToElementBySelector( - content: IonContent, + scrollToElementBySelector( + container: HTMLElement | null, + content: IonContent | undefined, selector: string, scrollParentClass?: string, duration?: number, - ): Promise { - // @todo: This function is broken. Scroll element cannot be used because it uses shadow DOM so querySelector returns null. - // Also, traversing using parentElement doesn't work either, offsetParent isn't part of the parentElement tree. - try { - const scrollElement = await content.getScrollElement(); + ): boolean { + if (!container || !content) { + return false; + } - const position = this.getElementXY(scrollElement, selector, scrollParentClass); + try { + const position = this.getElementXY(container, selector, scrollParentClass); if (!position) { return false; } @@ -1152,16 +1154,13 @@ export class CoreDomUtilsProvider { /** * Search for an input with error (core-input-error directive) and scrolls to it if found. * + * @param container The element that contains the element that must be scrolled. * @param content The content that must be scrolled. * @param scrollParentClass Parent class where to stop calculating the position. Default inner-scroll. * @return True if the element is found, false otherwise. */ - async scrollToInputError(content?: IonContent, scrollParentClass?: string): Promise { - if (!content) { - return false; - } - - return this.scrollToElementBySelector(content, '.core-input-error', scrollParentClass); + scrollToInputError(container: HTMLElement | null, content?: IonContent, scrollParentClass?: string): boolean { + return this.scrollToElementBySelector(container, content, '.core-input-error', scrollParentClass); } /**