diff --git a/src/defaultCss/defaultV2Css.ts b/src/defaultCss/defaultV2Css.ts index c1996f7acf..de6b8cf1ae 100644 --- a/src/defaultCss/defaultV2Css.ts +++ b/src/defaultCss/defaultV2Css.ts @@ -522,6 +522,9 @@ export var defaultV2Css = { rootDropdown: "sd-scrollable-container sd-scrollable-container--compact sd-selectbase", root: "sd-scrollable-container sd-rating", rootWrappable: "sd-scrollable-container sd-rating sd-rating--wrappable", + rootLabelsTop: "sd-rating--labels-top", + rootLabelsBottom: "sd-rating--labels-bottom", + rootLabelsDiagonal: "sd-rating--labels-diagonal", item: "sd-rating__item", itemOnError: "sd-rating__item--error", itemHover: "sd-rating__item--allowhover", diff --git a/src/defaultV2-theme/blocks/sd-rating.scss b/src/defaultV2-theme/blocks/sd-rating.scss index 36708709a8..7772956104 100644 --- a/src/defaultV2-theme/blocks/sd-rating.scss +++ b/src/defaultV2-theme/blocks/sd-rating.scss @@ -27,6 +27,69 @@ min-width: 0; } } + + &.sd-rating--labels-top { + fieldset { + padding-top: calcSize(4.5); + position: relative; + .sd-rating__min-text { + position: absolute; + margin: 0; + left:0; + top: 0; + border: 0; + } + .sd-rating__max-text { + position: absolute; + margin: 0; + right:0; + top: 0; + border: 0; + } + } + } + + &.sd-rating--labels-bottom { + fieldset { + padding-bottom: calcSize(4.5); + position: relative; + .sd-rating__min-text { + position: absolute; + margin: 0; + left:0; + bottom: 0; + border: 0; + } + .sd-rating__max-text { + position: absolute; + margin: 0; + right:0; + bottom: 0; + border: 0; + } + } + } + &.sd-rating--labels-diagonal { + fieldset { + padding-top: calcSize(4.5); + padding-bottom: calcSize(4.5); + position: relative; + .sd-rating__min-text { + position: absolute; + margin: 0; + left:0; + top: 0; + border: 0; + } + .sd-rating__max-text { + position: absolute; + margin: 0; + right:0; + bottom: 0; + border: 0; + } + } + } } .sd-rating--small { diff --git a/src/question_rating.ts b/src/question_rating.ts index 037f458e55..109790b97e 100644 --- a/src/question_rating.ts +++ b/src/question_rating.ts @@ -416,9 +416,10 @@ export class QuestionRatingModel extends Question { } /** * Specifies a description for the minimum (first) rate value. + * @see rateDescriptionLocation + * @see displayRateDescriptionsAsExtremeItems * @see rateValues * @see rateMin - * @see displayRateDescriptionsAsExtremeItems */ public get minRateDescription(): string { return this.getLocalizableStringText("minRateDescription"); @@ -432,9 +433,10 @@ export class QuestionRatingModel extends Question { } /** * Specifies a description for the maximum (last) rate value. + * @see rateDescriptionLocation + * @see displayRateDescriptionsAsExtremeItems * @see rateValues * @see rateMax - * @see displayRateDescriptionsAsExtremeItems */ public get maxRateDescription(): string { return this.getLocalizableStringText("maxRateDescription"); @@ -457,15 +459,14 @@ export class QuestionRatingModel extends Question { } /** - * Specifies whether to display `minRateDescription` and `maxRateDescription` values as captions for buttons that correspond to the extreme (first and last) rate values. + * Specifies whether to display [`minRateDescription`](https://surveyjs.io/form-library/documentation/api-reference/rating-scale-question-model#minRateDescription) and [`maxRateDescription`](https://surveyjs.io/form-library/documentation/api-reference/rating-scale-question-model#maxRateDescription) values as captions for buttons that correspond to the extreme (first and last) rate values. * * Default value: `false` * * If this property is disabled, the `minRateDescription` and `maxRateDescription` values are displayed as plain non-clickable texts. * * If any of the `minRateDescription` and `maxRateDescription` properties is empty, the corresponding rate value's `value` or `text` is displayed as a button caption. - * @see minRateDescription - * @see maxRateDescription + * @see rateDescriptionLocation * @see rateMin * @see rateMax * @see rateValues @@ -496,6 +497,19 @@ export class QuestionRatingModel extends Question { } }) displayMode: "dropdown" | "buttons" | "auto"; + /** + * Specifies the alignment of [`minRateDescription`](https://surveyjs.io/form-library/documentation/api-reference/rating-scale-question-model#minRateDescription) and [`maxRateDescription`](https://surveyjs.io/form-library/documentation/api-reference/rating-scale-question-model#maxRateDescription) texts. + * + * Possible values: + * + * - `"leftRight"` (default) - Aligns `minRateDescription` to the left of rate values and `maxRateDescription` to their right. + * - `"top"` - Displays the descriptions above the minimum and maximum rate values. + * - `"bottom"` - Displays both descriptions below the minimum and maximum rate values. + * - `"topBottom"` - Displays `minRateDescription` above the minimum rate value and `maxRateDescription` below the maximum rate value. + * @see displayRateDescriptionsAsExtremeItems + */ + @property() rateDescriptionLocation: "leftRight" | "top" | "bottom" | "topBottom"; + /** * Specifies the visual representation of rate values. * @@ -607,9 +621,15 @@ export class QuestionRatingModel extends Question { public get ratingRootCss(): string { const baseClass = ((this.displayMode == "buttons" || (!!this.survey && this.survey.isDesignMode)) && this.cssClasses.rootWrappable) ? this.cssClasses.rootWrappable : this.cssClasses.root; - + let rootClassModifier = ""; + if(this.hasMaxLabel || this.hasMinLabel) { + if (this.rateDescriptionLocation == "top") rootClassModifier = this.cssClasses.rootLabelsTop; + if (this.rateDescriptionLocation == "bottom") rootClassModifier = this.cssClasses.rootLabelsBottom; + if (this.rateDescriptionLocation == "topBottom") rootClassModifier = this.cssClasses.rootLabelsDiagonal; + } return new CssClassBuilder() .append(baseClass) + .append(rootClassModifier) .append(this.cssClasses.itemSmall, this.itemSmallMode && this.rateType != "labels") .toString(); } @@ -970,6 +990,11 @@ Serializer.addClass( return obj.rateType == "labels"; } }, + { + name: "rateDescriptionLocation", + default: "leftRight", + choices: ["leftRight", "top", "bottom", "topBottom"], + }, { name: "displayMode", default: "auto", diff --git a/tests/question_ratingtests.ts b/tests/question_ratingtests.ts index 36d4e8d859..d63927c7d1 100644 --- a/tests/question_ratingtests.ts +++ b/tests/question_ratingtests.ts @@ -1301,6 +1301,18 @@ QUnit.test("check rating in-matrix mode styles", (assert) => { settings.matrix.rateSize = "small"; }); +QUnit.test("check rating in-matrix mode styles", (assert) => { + const survey = new SurveyModel({ questions: [{ type: "rating", name: "q1" }] }); + const q1 = survey.getQuestionByName("q1") as QuestionRatingModel; + q1.cssClasses.root = "sv_q"; + q1.cssClasses.rootLabelsTop = "sv_q__top"; + assert.equal(q1.ratingRootCss, "sv_q"); + q1.rateDescriptionLocation = "top"; + assert.equal(q1.ratingRootCss, "sv_q"); + q1.maxRateDescription = "Bad"; + assert.equal(q1.ratingRootCss, "sv_q sv_q__top"); +}); + QUnit.test("check rating triggerResponsiveness method", (assert) => { const ResizeObserver = window.ResizeObserver; window.ResizeObserver = CustomResizeObserver; diff --git a/visualRegressionTests/tests/defaultV2/etalons/question-rating-labels-bottom.png b/visualRegressionTests/tests/defaultV2/etalons/question-rating-labels-bottom.png new file mode 100644 index 0000000000..d229a15bdc Binary files /dev/null and b/visualRegressionTests/tests/defaultV2/etalons/question-rating-labels-bottom.png differ diff --git a/visualRegressionTests/tests/defaultV2/etalons/question-rating-labels-diagonal.png b/visualRegressionTests/tests/defaultV2/etalons/question-rating-labels-diagonal.png new file mode 100644 index 0000000000..4c9ab9fa35 Binary files /dev/null and b/visualRegressionTests/tests/defaultV2/etalons/question-rating-labels-diagonal.png differ diff --git a/visualRegressionTests/tests/defaultV2/etalons/question-rating-labels-top.png b/visualRegressionTests/tests/defaultV2/etalons/question-rating-labels-top.png new file mode 100644 index 0000000000..1e8ce9a743 Binary files /dev/null and b/visualRegressionTests/tests/defaultV2/etalons/question-rating-labels-top.png differ diff --git a/visualRegressionTests/tests/defaultV2/rating.ts b/visualRegressionTests/tests/defaultV2/rating.ts index 0aa1d25c2f..0d2a88850a 100644 --- a/visualRegressionTests/tests/defaultV2/rating.ts +++ b/visualRegressionTests/tests/defaultV2/rating.ts @@ -746,4 +746,49 @@ frameworks.forEach(framework => { }); }); + test("Check rating rate descriptions position", async (t) => { + await wrapVisualTest(t, async (t, comparer) => { + await t.resizeWindow(1920, 1080); + const focusBody = ClientFunction(() => { document.body.focus(); }); + await initSurvey(framework, { + showQuestionNumbers: "off", + width: "900px", + questions: [ + { + "type": "rating", + "name": "question2", + "title": "How likely are you to recommend us to a friend or colleague?", + "rateMax": 10, + "minRateDescription": "Not at all likely", + "maxRateDescription": "Extremely likely", + "rateDescriptionLocation": "top" + }, + { + "type": "rating", + "name": "question3", + "title": "How likely are you to recommend us to a friend or colleague?", + "rateMax": 10, + "minRateDescription": "Not at all likely", + "maxRateDescription": "Extremely likely", + "rateDescriptionLocation": "bottom" + }, + { + "type": "rating", + "name": "question4", + "title": "How likely are you to recommend us to a friend or colleague?", + "rateMax": 10, + "minRateDescription": "Not at all likely", + "maxRateDescription": "Extremely likely", + "rateDescriptionLocation": "topBottom" + } + ] + }); + + const questionRoot = Selector(".sd-question"); + await takeElementScreenshot("question-rating-labels-top.png", questionRoot.nth(0), t, comparer); + await takeElementScreenshot("question-rating-labels-bottom.png", questionRoot.nth(1), t, comparer); + await takeElementScreenshot("question-rating-labels-diagonal.png", questionRoot.nth(2), t, comparer); + }); + }); + });