From 655357b78e0ffc54f7aeb1879c6344412a4b5eff Mon Sep 17 00:00:00 2001 From: Andrew Telnov Date: Wed, 18 Oct 2023 16:54:40 +0300 Subject: [PATCH 1/2] Custom Components - Question input is not highlighted when a validation error occurs fix #7180 --- src/question.ts | 14 +++++++++----- src/question_baseselect.ts | 2 +- src/question_boolean.ts | 2 +- src/question_custom.ts | 1 + src/question_dropdown.ts | 2 +- src/question_file.ts | 2 +- src/question_matrix.ts | 4 ++-- src/question_ranking.ts | 2 +- src/question_rating.ts | 4 ++-- src/question_tagbox.ts | 2 +- src/question_textbase.ts | 2 +- tests/question_customtests.ts | 18 ++++++++++++++++++ 12 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/question.ts b/src/question.ts index 5939f09de0..ca1f309300 100644 --- a/src/question.ts +++ b/src/question.ts @@ -68,6 +68,7 @@ export class Question extends SurveyElement focusCallback: () => void; surveyLoadCallback: () => void; displayValueCallback: (text: string) => string; + hasCssErrorCallback: () => boolean = (): boolean => false; private defaultValueRunner: ExpressionRunner; private isChangingViaDefaultValue: boolean; @@ -962,7 +963,7 @@ export class Question extends SurveyElement this.setPropertyValue("cssRoot", val); } protected getCssRoot(cssClasses: { [index: string]: string }): string { - const hasError = this.errors.length > 0; + const hasError = this.hasCssError(); return new CssClassBuilder() .append(super.getCssRoot(cssClasses)) .append(this.isFlowLayout && !this.isDesignMode @@ -1071,6 +1072,9 @@ export class Question extends SurveyElement .append(cssClasses.error.locationBottom, this.showErrorOnBottom) .toString(); } + protected hasCssError(): boolean { + return this.errors.length > 0 || this.hasCssErrorCallback(); + } public getRootCss(): string { return new CssClassBuilder() .append(this.cssRoot) @@ -2538,7 +2542,7 @@ export class Question extends SurveyElement public get ariaInvalid() { if (this.isNewA11yStructure) return null; - return this.errors.length > 0 ? "true" : "false"; + return this.hasCssError() ? "true" : "false"; } public get ariaLabelledBy(): string { if (this.isNewA11yStructure) return null; @@ -2555,7 +2559,7 @@ export class Question extends SurveyElement public get ariaDescribedBy(): string { if (this.isNewA11yStructure) return null; - return this.errors.length > 0 ? this.id + "_errors" : null; + return this.hasCssError() ? this.id + "_errors" : null; } //EO a11y @@ -2567,7 +2571,7 @@ export class Question extends SurveyElement return this.isRequired ? "true" : "false"; } public get a11y_input_ariaInvalid(): "true" | "false" { - return this.errors.length > 0 ? "true" : "false"; + return this.hasCssError() ? "true" : "false"; } public get a11y_input_ariaLabel(): string { if (this.hasTitle && !this.parentQuestion) { @@ -2584,7 +2588,7 @@ export class Question extends SurveyElement } } public get a11y_input_ariaDescribedBy(): string { - return this.errors.length > 0 ? this.id + "_errors" : null; + return this.hasCssError() ? this.id + "_errors" : null; } //EO new a11y } diff --git a/src/question_baseselect.ts b/src/question_baseselect.ts index 07daa788f7..7e092623fe 100644 --- a/src/question_baseselect.ts +++ b/src/question_baseselect.ts @@ -1541,7 +1541,7 @@ export class QuestionSelectBase extends Question { .append(this.cssClasses.item) .append(this.cssClasses.itemInline, !this.hasColumns && this.colCount === 0) .append("sv-q-col-" + this.getCurrentColCount(), !this.hasColumns && this.colCount !== 0) - .append(this.cssClasses.itemOnError, this.errors.length > 0); + .append(this.cssClasses.itemOnError, this.hasCssError()); const isDisabled = this.isReadOnly || !item.isEnabled; const isChecked = this.isItemSelected(item) || diff --git a/src/question_boolean.ts b/src/question_boolean.ts index aca32799dd..cb1281c191 100644 --- a/src/question_boolean.ts +++ b/src/question_boolean.ts @@ -175,7 +175,7 @@ export class QuestionBooleanModel extends Question { private getItemCssValue(css: any): string { return new CssClassBuilder() .append(css.item) - .append(css.itemOnError, this.errors.length > 0) + .append(css.itemOnError, this.hasCssError()) .append(css.itemDisabled, this.isReadOnly) .append(css.itemHover, !this.isDesignMode) .append(css.itemChecked, !!this.booleanValue) diff --git a/src/question_custom.ts b/src/question_custom.ts index c255cdb9fe..f564cb2dc3 100644 --- a/src/question_custom.ts +++ b/src/question_custom.ts @@ -689,6 +689,7 @@ export class QuestionCustomModel extends QuestionCustomModelBase { res.onUpdateCssClassesCallback = (css: any): void => { this.onUpdateQuestionCssClasses(res, css); }; + res.hasCssErrorCallback = (): boolean => this.errors.length > 0; } return res; diff --git a/src/question_dropdown.ts b/src/question_dropdown.ts index f5c511efa2..65ce87f054 100644 --- a/src/question_dropdown.ts +++ b/src/question_dropdown.ts @@ -222,7 +222,7 @@ export class QuestionDropdownModel extends QuestionSelectBase { return new CssClassBuilder() .append(this.cssClasses.control) .append(this.cssClasses.controlEmpty, this.isEmpty()) - .append(this.cssClasses.onError, this.errors.length > 0) + .append(this.cssClasses.onError, this.hasCssError()) .append(this.cssClasses.controlDisabled, this.isReadOnly) .append(this.cssClasses.controlInputFieldComponent, !!this.inputFieldComponentName) .toString(); diff --git a/src/question_file.ts b/src/question_file.ts index 49fe86b82a..aca2db6ee2 100644 --- a/src/question_file.ts +++ b/src/question_file.ts @@ -805,7 +805,7 @@ export class QuestionFileModel extends Question { public getFileDecoratorCss(): string { return new CssClassBuilder() .append(this.cssClasses.fileDecorator) - .append(this.cssClasses.onError, this.errors.length > 0) + .append(this.cssClasses.onError, this.hasCssError()) .append(this.cssClasses.fileDecoratorDrag, this.isDragging) .toString(); } diff --git a/src/question_matrix.ts b/src/question_matrix.ts index 9dec95bbef..1b0a482df9 100644 --- a/src/question_matrix.ts +++ b/src/question_matrix.ts @@ -302,7 +302,7 @@ export class QuestionMatrixModel return new CssClassBuilder() .append(this.cssClasses.cell, this.hasCellText) .append(this.hasCellText ? this.cssClasses.cellText : this.cssClasses.label) - .append(this.cssClasses.itemOnError, !this.hasCellText && this.errors.length > 0) + .append(this.cssClasses.itemOnError, !this.hasCellText && this.hasCssError()) .append(this.hasCellText ? this.cssClasses.cellTextSelected : this.cssClasses.itemChecked, isChecked) .append(this.hasCellText ? this.cssClasses.cellTextDisabled : this.cssClasses.itemDisabled, isDisabled) .append(this.cssClasses.itemHover, allowHover && !this.hasCellText) @@ -419,7 +419,7 @@ export class QuestionMatrixModel ) { super.onCheckForErrors(errors, isOnValueChanged); if ( - (!isOnValueChanged || this.errors.length > 0) && + (!isOnValueChanged || this.hasCssError()) && this.hasErrorInRows() ) { errors.push(new RequiredInAllRowsError(null, this)); diff --git a/src/question_ranking.ts b/src/question_ranking.ts index b61f2860ab..3d0df183d2 100644 --- a/src/question_ranking.ts +++ b/src/question_ranking.ts @@ -47,7 +47,7 @@ export class QuestionRankingModel extends QuestionCheckboxModel { .append(this.cssClasses.rootMobileMod, IsMobile) .append(this.cssClasses.rootDisabled, this.isReadOnly) .append(this.cssClasses.rootDesignMode, !!this.isDesignMode) - .append(this.cssClasses.itemOnError, this.errors.length > 0) + .append(this.cssClasses.itemOnError, this.hasCssError()) .append(this.cssClasses.rootDragHandleAreaIcon, settings.rankingDragHandleArea === "icon") .append(this.cssClasses.rootSelectToRankMod, this.selectToRankEnabled) .append(this.cssClasses.rootSelectToRankAlignHorizontal, this.selectToRankEnabled && this.selectToRankAreasLayout === "horizontal") diff --git a/src/question_rating.ts b/src/question_rating.ts index f168ea4d6c..4f79bd3e53 100644 --- a/src/question_rating.ts +++ b/src/question_rating.ts @@ -726,7 +726,7 @@ export class QuestionRatingModel extends Question { .append(itemScaleColoredClass, this.scaleColorMode == "colored") .append(itemRateColoredClass, this.rateColorMode == "scale" && isSelected) .append(itemUnhighlightedClass, isUnhighlighted) - .append(itemitemOnErrorClass, this.errors.length > 0) + .append(itemitemOnErrorClass, this.hasCssError()) .append(itemSmallClass, this.itemSmallMode) .append(this.cssClasses.itemFixedSize, hasFixedSize) .toString(); @@ -737,7 +737,7 @@ export class QuestionRatingModel extends Question { return new CssClassBuilder() .append(this.cssClasses.control) .append(this.cssClasses.controlEmpty, this.isEmpty()) - .append(this.cssClasses.onError, this.errors.length > 0) + .append(this.cssClasses.onError, this.hasCssError()) .append(this.cssClasses.controlDisabled, this.isReadOnly) .toString(); } diff --git a/src/question_tagbox.ts b/src/question_tagbox.ts index 9553742654..3dc011d4e4 100644 --- a/src/question_tagbox.ts +++ b/src/question_tagbox.ts @@ -131,7 +131,7 @@ export class QuestionTagboxModel extends QuestionCheckboxModel { return new CssClassBuilder() .append(this.cssClasses.control) .append(this.cssClasses.controlEmpty, this.isEmpty()) - .append(this.cssClasses.onError, this.errors.length > 0) + .append(this.cssClasses.onError, this.hasCssError()) .append(this.cssClasses.controlDisabled, this.isReadOnly) .toString(); } diff --git a/src/question_textbase.ts b/src/question_textbase.ts index 565efd7d21..45dea8fb21 100644 --- a/src/question_textbase.ts +++ b/src/question_textbase.ts @@ -136,7 +136,7 @@ export class QuestionTextBase extends Question { public getControlClass(): string { return new CssClassBuilder() .append(this.cssClasses.root) - .append(this.cssClasses.onError, this.errors.length > 0) + .append(this.cssClasses.onError, this.hasCssError()) .append(this.cssClasses.controlDisabled, this.isReadOnly) .toString(); } diff --git a/tests/question_customtests.ts b/tests/question_customtests.ts index 70e142a3c0..1676ee3812 100644 --- a/tests/question_customtests.ts +++ b/tests/question_customtests.ts @@ -2392,3 +2392,21 @@ QUnit.test("Complex: onHidingContent", function (assert) { assert.equal(counter, 2, "onComplete"); ComponentCollection.Instance.clear(); }); +QUnit.test("Single: Apply error css", function (assert) { + const json = { + name: "newquestion", + questionJSON: { type: "text" }, + }; + ComponentCollection.Instance.add(json); + const survey = new SurveyModel({ + elements: [{ type: "newquestion", name: "q1", isRequired: true }], + }); + const q = survey.getAllQuestions()[0]; + const qText = q.contentQuestion; + const errorCss = qText.cssClasses.onError; + assert.ok(errorCss, "error css is not empty"); + assert.equal(qText.getControlClass().indexOf(errorCss) < 0, true, "errors is not here"); + q.validate(true); + assert.equal(qText.getControlClass().indexOf(errorCss) > -1, true, "errors is here"); + ComponentCollection.Instance.clear(); +}); From a97a49739be1c5f1c53e1455736ef713b8844a21 Mon Sep 17 00:00:00 2001 From: Andrew Telnov Date: Wed, 18 Oct 2023 17:08:05 +0300 Subject: [PATCH 2/2] Fix unit test #7180 --- tests/question_customtests.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/question_customtests.ts b/tests/question_customtests.ts index 1676ee3812..1be9d27695 100644 --- a/tests/question_customtests.ts +++ b/tests/question_customtests.ts @@ -2398,13 +2398,15 @@ QUnit.test("Single: Apply error css", function (assert) { questionJSON: { type: "text" }, }; ComponentCollection.Instance.add(json); - const survey = new SurveyModel({ + const errorCss = "single_error"; + const survey = new SurveyModel(); + survey.css = { text: { onError: errorCss } }; + survey.fromJSON({ elements: [{ type: "newquestion", name: "q1", isRequired: true }], }); const q = survey.getAllQuestions()[0]; const qText = q.contentQuestion; - const errorCss = qText.cssClasses.onError; - assert.ok(errorCss, "error css is not empty"); + assert.equal(qText.cssClasses.onError, errorCss, "error css is correct"); assert.equal(qText.getControlClass().indexOf(errorCss) < 0, true, "errors is not here"); q.validate(true); assert.equal(qText.getControlClass().indexOf(errorCss) > -1, true, "errors is here");