diff --git a/src/base-interfaces.ts b/src/base-interfaces.ts index ef4c4e991a..7cba8af6cd 100644 --- a/src/base-interfaces.ts +++ b/src/base-interfaces.ts @@ -45,6 +45,7 @@ export interface ISurveyErrorOwner extends ILocalizableOwner { export interface ISurvey extends ITextProcessor, ISurveyErrorOwner { getSkeletonComponentName(element: ISurveyElement): string; currentPage: IPage; + activePage: IPage; pages: Array; getCss(): any; isPageStarted(page: IPage): boolean; @@ -77,6 +78,7 @@ export interface ISurvey extends ITextProcessor, ISurveyErrorOwner { oldName: string, oldValueName: string ): any; + focusQuestionByInstance(question: IQuestion, onError: boolean): boolean; validateQuestion(question: IQuestion): SurveyError; validatePanel(panel: IPanel): SurveyError; hasVisibleQuestionByValueName(valueName: string): boolean; diff --git a/src/panel.ts b/src/panel.ts index e369d1ab56..f6cf408ad7 100644 --- a/src/panel.ts +++ b/src/panel.ts @@ -524,6 +524,16 @@ export class PanelModelBase extends SurveyElement return this.questionsValue; } + public getQuestions(includeNested: boolean): Array { + const res = this.questions; + if(!includeNested) return res; + const res2: Array = []; + res.forEach(q => { + res2.push(q); + q.getNestedQuestions().forEach(nQ => res2.push(nQ)); + }); + return res2; + } protected getValidName(name: string): string { if (!!name) return name.trim(); return name; diff --git a/src/question.ts b/src/question.ts index fa6392e363..814b75e25d 100644 --- a/src/question.ts +++ b/src/question.ts @@ -249,6 +249,7 @@ export class Question extends SurveyElement * Returns a page to which the question belongs and allows you to move this question to a different page. */ public get page(): IPage { + if(!!this.parentQuestion) return this.parentQuestion.page; return this.getPage(this.parent); } public set page(val: IPage) { @@ -969,13 +970,9 @@ export class Question extends SurveyElement public focus(onError: boolean = false): void { if (this.isDesignMode || !this.isVisible || !this.survey) return; let page = this.page; - if(!page && !!this.parentQuestion) { - page = this.parentQuestion.page; - } - let shouldChangePage = !!page && this.survey.currentPage !== page; + const shouldChangePage = !!page && this.survey.activePage !== page; if(shouldChangePage) { - this.survey.currentPage = page; - setTimeout(() => this.focuscore(onError), 0); + this.survey.focusQuestionByInstance(this, onError); } else { this.focuscore(onError); } diff --git a/src/survey-element.ts b/src/survey-element.ts index 18be5271eb..51cdac9fc7 100644 --- a/src/survey-element.ts +++ b/src/survey-element.ts @@ -201,7 +201,7 @@ export class SurveyElement extends SurveyElementCore implements ISurvey const { root } = settings.environment; if (!root) return false; const el = root.getElementById(elementId); - if (el && !(el)["disabled"]) { + if (el && !(el)["disabled"] && el.style.display !== "none") { el.focus(); return true; } diff --git a/src/survey.ts b/src/survey.ts index b9a78c4c88..327f78506e 100644 --- a/src/survey.ts +++ b/src/survey.ts @@ -3081,7 +3081,7 @@ export class SurveyModel extends SurveyElementCore * @see focusFirstQuestionAutomatic */ public focusFirstQuestion() { - if (this.isFocusingQuestion) return; + if (this.focusingQuestionInfo) return; var page = this.activePage; if (page) { page.scrollToTop(); @@ -3094,7 +3094,7 @@ export class SurveyModel extends SurveyElementCore if (doScroll) { page.scrollToTop(); } - if (this.isCurrentPageRendering && this.focusFirstQuestionAutomatic && !this.isFocusingQuestion) { + if (this.isCurrentPageRendering && this.focusFirstQuestionAutomatic && !this.focusingQuestionInfo) { page.focusFirstQuestion(); this.isCurrentPageRendering = false; } @@ -3614,9 +3614,8 @@ export class SurveyModel extends SurveyElementCore } } if (focusOnFirstError && !!firstErrorPage) { - this.currentPage = firstErrorPage; - var questions = firstErrorPage.questions; - for (var i = 0; i < questions.length; i++) { + const questions = firstErrorPage.getQuestions(true); + for (let i = 0; i < questions.length; i++) { if (questions[i].errors.length > 0) { questions[i].focus(true); break; @@ -4216,6 +4215,7 @@ export class SurveyModel extends SurveyElementCore */ public start(): boolean { if (!this.firstPageIsStarted) return false; + this.isCurrentPageRendering = true; if (this.checkIsPageHasErrors(this.startedPage, true)) return false; this.isStartedState = false; this.startTimerFromUI(); @@ -4550,12 +4550,10 @@ export class SurveyModel extends SurveyElementCore private isFirstPageRendering: boolean = true; private isCurrentPageRendering: boolean = true; afterRenderPage(htmlElement: HTMLElement) { - if (!this.isDesignMode && !this.isFocusingQuestion) { + if (!this.isDesignMode && !this.focusingQuestionInfo) { setTimeout(() => this.scrollToTopOnPageChange(!this.isFirstPageRendering), 1); } - while (this.afterRenderPageTasks.length > 0) { - this.afterRenderPageTasks.shift()(); - } + this.focusQuestionInfo(); this.isFirstPageRendering = false; if (this.onAfterRenderPage.isEmpty) return; this.onAfterRenderPage.fire(this, { @@ -6958,8 +6956,7 @@ export class SurveyModel extends SurveyElementCore triggerExecuted(trigger: Trigger): void { this.onTriggerExecuted.fire(this, { trigger: trigger }); } - private isFocusingQuestion: boolean; - private afterRenderPageTasks: Array<() => void> = []; + private focusingQuestionInfo: any; private isMovingQuestion: boolean; public startMovingQuestion(): void { this.isMovingQuestion = true; @@ -6979,24 +6976,30 @@ export class SurveyModel extends SurveyElementCore * @see focusFirstQuestionAutomatic */ public focusQuestion(name: string): boolean { - var question = this.getQuestionByName(name, true); + return this.focusQuestionByInstance(this.getQuestionByName(name, true)); + } + focusQuestionByInstance(question: Question, onError: boolean = false): boolean { if (!question || !question.isVisible || !question.page) return false; - this.isFocusingQuestion = true; + const oldQuestion = this.focusingQuestionInfo?.question; + if(oldQuestion === question) return false; + this.focusingQuestionInfo = { question: question, onError: onError }; this.skippedPages.push({ from: this.currentPage, to: question.page }); - const isNeedWaitForPageRendered = this.currentPage !== question.page; - const focusQuestionFunc = () => { - question.focus(); - this.isFocusingQuestion = false; - this.isCurrentPageRendering = false; - }; - this.afterRenderPageTasks.push(focusQuestionFunc); - this.currentPage = question.page; + const isNeedWaitForPageRendered = this.activePage !== question.page && !question.page.isStartPage; + if(isNeedWaitForPageRendered) { + this.currentPage = question.page; + } if (!isNeedWaitForPageRendered) { - focusQuestionFunc(); - this.afterRenderPageTasks.splice(this.afterRenderPageTasks.indexOf(focusQuestionFunc), 1); + this.focusQuestionInfo(); } return true; } + private focusQuestionInfo(): void { + const question = this.focusingQuestionInfo?.question; + if(!!question && !question.isDisposed) { + question.focus(this.focusingQuestionInfo.onError); + } + this.focusingQuestionInfo = undefined; + } public questionEditFinishCallback(question: Question, event: any) { const enterKeyAction = this.enterKeyAction || settings.enterKeyAction; diff --git a/testCafe/validation/focusErroredInput.js b/testCafe/validation/focusErroredInput.js new file mode 100644 index 0000000000..b95b485948 --- /dev/null +++ b/testCafe/validation/focusErroredInput.js @@ -0,0 +1,125 @@ +import { frameworks, url, initSurvey, getSurveyResult } from "../helper"; +import { Selector, ClientFunction, fixture, test } from "testcafe"; +const title = "focus input with Error"; +const json1 = { + "pages": [ + { + "name": "page1", + "elements": [ + { + "type": "text", + "name": "question1" + }, + { + "type": "boolean", + "name": "question2" + }, + + { + "type": "text", + "name": "q1", + "validators": [{ "type": "numeric", "text": "Enter only numbers" }] + } + ] + }, + { + "name": "page2", + "elements": [ + { + "type": "text", + "name": "question3" + } + ] + } + ], + "checkErrorsMode": "onComplete" +}; +const json2 = { + "pages": [ + { + "name": "page1", + "elements": [ + { + "type": "text", + "name": "question1" + }, + { + "type": "boolean", + "name": "question2" + }, + + { + "type": "matrixdynamic", + "name": "matrix", + "rowCount": 1, + "columns": [ + { "cellType": "text", "name": "col1", + "validators": [{ "type": "numeric", "text": "Enter only numbers" }] } + ] + } + ] + }, + { + "name": "page2", + "elements": [ + { + "type": "text", + "name": "question3" + } + ] + } + ], + "checkErrorsMode": "onComplete" +}; + +frameworks.forEach(framework => { + fixture`${framework} ${title}`.page`${url}${framework}`.beforeEach( + async t => { + await initSurvey(framework, json1); + } + ); + + test("validate on error", async t => { + let surveyResult; + + await t + .pressKey("tab") + .pressKey("tab") + .pressKey("a") + .click("input[value=\"Next\"]") + .click("input[value=\"Complete\"]") + .pressKey("backspace") + .pressKey("1") + .click("input[value=\"Next\"]") + .click("input[value=\"Complete\"]"); + + surveyResult = await getSurveyResult(); + await t.expect(surveyResult).eql({ q1: "1" }); + }); +}); + +frameworks.forEach(framework => { + fixture`${framework} ${title}`.page`${url}${framework}`.beforeEach( + async t => { + await initSurvey(framework, json2); + } + ); + + test("validate on error in matrix", async t => { + let surveyResult; + + await t + .pressKey("tab") + .pressKey("tab") + .pressKey("a") + .click("input[value=\"Next\"]") + .click("input[value=\"Complete\"]") + .pressKey("backspace") + .pressKey("1") + .click("input[value=\"Next\"]") + .click("input[value=\"Complete\"]"); + + surveyResult = await getSurveyResult(); + await t.expect(surveyResult).eql({ matrix: [{ col1: "1" }] }); + }); +}); diff --git a/tests/surveytests.ts b/tests/surveytests.ts index f38e16732b..1aa856cbe1 100644 --- a/tests/surveytests.ts +++ b/tests/surveytests.ts @@ -1469,6 +1469,7 @@ QUnit.test("survey.checkErrorsMode = 'onComplete'", function (assert) { assert.equal(survey.currentPageNo, 1, "Ignore error on the first page"); survey.completeLastPage(); assert.equal(survey.currentPageNo, 0, "Move to first page with the error"); + survey.afterRenderPage({}); survey.nextPage(); assert.equal(survey.currentPageNo, 1, "Ignore error on the first page, #2"); @@ -1478,6 +1479,7 @@ QUnit.test("survey.checkErrorsMode = 'onComplete'", function (assert) { 0, "Move to first page with the error, #2" ); + survey.afterRenderPage({}); survey.setValue("q1", "john.snow@nightwatch.org"); survey.nextPage(); @@ -13658,6 +13660,7 @@ QUnit.test( survey.nextPage(); survey.nextPage(); survey.completeLastPage(); + survey.afterRenderPage({}); assert.equal(survey.currentPageNo, 0, "The first page is active"); assert.equal( survey.getQuestionByName("q1").inputId, diff --git a/visualRegressionTests/tests/defaultV2/etalons/survey-responsive-timer.png b/visualRegressionTests/tests/defaultV2/etalons/survey-responsive-timer.png index ef0ef0a45c..7c117ab6c4 100644 Binary files a/visualRegressionTests/tests/defaultV2/etalons/survey-responsive-timer.png and b/visualRegressionTests/tests/defaultV2/etalons/survey-responsive-timer.png differ diff --git a/visualRegressionTests/tests/defaultV2/etalons/survey-timer-without-progress.png b/visualRegressionTests/tests/defaultV2/etalons/survey-timer-without-progress.png index c9ab9235e1..88662ad4c6 100644 Binary files a/visualRegressionTests/tests/defaultV2/etalons/survey-timer-without-progress.png and b/visualRegressionTests/tests/defaultV2/etalons/survey-timer-without-progress.png differ diff --git a/visualRegressionTests/tests/defaultV2/etalons/survey-timer.png b/visualRegressionTests/tests/defaultV2/etalons/survey-timer.png index 2f29dfe5a2..f88dc8f84e 100644 Binary files a/visualRegressionTests/tests/defaultV2/etalons/survey-timer.png and b/visualRegressionTests/tests/defaultV2/etalons/survey-timer.png differ diff --git a/visualRegressionTests/tests/defaultV2/survey.ts b/visualRegressionTests/tests/defaultV2/survey.ts index f80a95ab08..d41e91cdc5 100644 --- a/visualRegressionTests/tests/defaultV2/survey.ts +++ b/visualRegressionTests/tests/defaultV2/survey.ts @@ -687,13 +687,14 @@ frameworks.forEach(framework => { test("Check survey notifier error type", async (t) => { await wrapVisualTest(t, async (t, comparer) => { + await ClientFunction(() => { (window).Survey.settings.notifications.lifetime = 10000; })(); await t.resizeWindow(1920, 900); await initSurvey(framework, notifierJson, { onComplete: (_sender, options) => { options.isCompleteOnTrigger = false; options.showDataSaving(); let fail = true; - new Promise((resolve, reject) => { setTimeout(fail ? reject : resolve, 5000); }).then( + new Promise((resolve, reject) => { setTimeout(fail ? reject : resolve, 500); }).then( () => { options.showDataSavingSuccess(); }, () => { options.showDataSavingError(); } ); @@ -701,6 +702,7 @@ frameworks.forEach(framework => { await setData({ nps_score: 4 }); await t.click("input[value=\"Complete\"]"); await takeElementScreenshot("save-data-error.png", Selector(".sv-save-data_root.sv-save-data_error"), t, comparer); + await ClientFunction(() => { (window).Survey.settings.notifications.lifetime = 2000; })(); }); }); @@ -713,7 +715,7 @@ frameworks.forEach(framework => { options.showDataSaving(); let fail = false; - new Promise((resolve, reject) => { setTimeout(fail ? reject : resolve, 5000); }).then( + new Promise((resolve, reject) => { setTimeout(fail ? reject : resolve, 500); }).then( () => { options.showDataSavingSuccess(); }, () => { options.showDataSavingError(); } );