diff --git a/e2e/survey/surveyWidth.spec.ts b/e2e/survey/surveyWidth.spec.ts index dc9f804d2a..a477386583 100644 --- a/e2e/survey/surveyWidth.spec.ts +++ b/e2e/survey/surveyWidth.spec.ts @@ -24,7 +24,7 @@ frameworks.forEach((framework) => { await expect(surveyBody).toHaveCSS("max-width", "455px"); }); - test("Check question width", async ({ page }) => { + test("Check question min-width, set it via the element property", async ({ page }) => { const questionDiv = page.locator(".sd-row > div"); await expect(questionDiv).toHaveCSS("min-width", "min(100%, 300px)"); @@ -34,5 +34,16 @@ frameworks.forEach((framework) => { await expect(questionDiv).toHaveCSS("min-width", "min(100%, 200px)"); }); + + test("Check question min-width, set it via the theme CSS variable", async ({ page }) => { + const questionDiv = page.locator(".sd-row > div"); + await expect(questionDiv).toHaveCSS("min-width", "min(100%, 300px)"); + + await page.evaluate(() => { + document.documentElement.style.setProperty("--sjs-element-min-width", "200px"); + }); + + await expect(questionDiv).toHaveCSS("min-width", "min(100%, 200px)"); + }); }); }); \ No newline at end of file diff --git a/packages/survey-angular-ui/src/element.component.html b/packages/survey-angular-ui/src/element.component.html index 268a047eec..f5e0dc39bf 100644 --- a/packages/survey-angular-ui/src/element.component.html +++ b/packages/survey-angular-ui/src/element.component.html @@ -1,5 +1,5 @@ -
+
\ No newline at end of file diff --git a/packages/survey-core/src/default-theme/blocks/sd-element.scss b/packages/survey-core/src/default-theme/blocks/sd-element.scss index 0f65ab4661..58219260a8 100644 --- a/packages/survey-core/src/default-theme/blocks/sd-element.scss +++ b/packages/survey-core/src/default-theme/blocks/sd-element.scss @@ -152,6 +152,19 @@ background-color: transparent; } +// The element is sized by the survey layout, so it must never grow past its row. Without this, +// a flex item's automatic minimum size is its content size, and wide content (a dynamic panel tab +// strip, for one) makes the element overflow the row instead of letting the content collapse. +.sd-element-wrapper { + max-width: 100%; +} + +// Below this width the element wraps onto a row of its own. Panels, grid layout columns and +// elements the survey does not size shrink to their content instead and do not get this class. +.sd-element-wrapper--min-width { + min-width: min(100%, #{$sd-element-min-width}); +} + @keyframes elementMoveIn { from { flex-basis: 0; diff --git a/packages/survey-core/src/default-theme/variables.scss b/packages/survey-core/src/default-theme/variables.scss index 591ed78b96..04e3b49ad6 100644 --- a/packages/survey-core/src/default-theme/variables.scss +++ b/packages/survey-core/src/default-theme/variables.scss @@ -62,6 +62,9 @@ $font-editorfont-size: var(--sjs2-typography-font-size-component-input-content); $sd-panel-normal-min-width: 496px; $sd-panel-medium-min-width: 176px; +// Below this width an element wraps onto its own row. Expressed in base units so that it follows the surface scale. +$sd-element-min-width: var(--sjs-element-min-width, calc(37.5 * var(--sjs2-base-unit-size))); + :root, :host { --sjs-transition-duration: 150ms; diff --git a/packages/survey-core/src/defaultCss/defaultCss.ts b/packages/survey-core/src/defaultCss/defaultCss.ts index 89d2091b8f..99415dc56c 100644 --- a/packages/survey-core/src/defaultCss/defaultCss.ts +++ b/packages/survey-core/src/defaultCss/defaultCss.ts @@ -181,6 +181,8 @@ export var defaultCss = { rowLeave: "sd-row--leave", rowReplace: "sd-row--replace", pageRow: "sd-page__row", + elementWrapper: "sd-element-wrapper", + elementWrapperMinWidth: "sd-element-wrapper--min-width", question: { contentEnter: "sd-element__content--enter", contentLeave: "sd-element__content--leave", diff --git a/packages/survey-core/src/panel.ts b/packages/survey-core/src/panel.ts index 26e3a8f9c3..01f8c56477 100644 --- a/packages/survey-core/src/panel.ts +++ b/packages/survey-core/src/panel.ts @@ -694,6 +694,7 @@ export class PanelModelBase extends SurveyElement return classes; } protected getIdPrefix(): string { return "sp"; } + protected get hasMinWidth(): boolean { return false; } public get isPanel(): boolean { return false; } @@ -2571,8 +2572,8 @@ Serializer.addClass( { name: "requiredErrorText:text", serializationProperty: "locRequiredErrorText" }, { name: "startWithNewLine:boolean", default: true }, { name: "width" }, - { name: "minWidth", defaultFunc: () => "auto" }, - { name: "maxWidth", defaultFunc: () => settings.maxWidth, onSettingValue: (obj: any, val: any): any => { return val || undefined; } }, + { name: "minWidth" }, + { name: "maxWidth" }, { name: "colSpan:number", visible: false, onSerializeValue: (obj) => { return obj.getPropertyValue("colSpan"); } }, { name: "effectiveColSpan:number", minValue: 1, isSerializable: false, diff --git a/packages/survey-core/src/question.ts b/packages/survey-core/src/question.ts index bf0b3be656..c59f6f2fdc 100644 --- a/packages/survey-core/src/question.ts +++ b/packages/survey-core/src/question.ts @@ -3265,8 +3265,8 @@ Serializer.addClass("question", [ { name: "useDisplayValuesInDynamicTexts:boolean", alternativeName: "useDisplayValuesInTitle", default: true, layout: "row" }, "visibleIf:condition", { name: "width" }, - { name: "minWidth", defaultFunc: () => settings.minWidth }, - { name: "maxWidth", defaultFunc: () => settings.maxWidth, onSettingValue: (obj: any, val: any): any => { return val || undefined; } }, + { name: "minWidth" }, + { name: "maxWidth" }, { name: "colSpan:number", visible: false, onSerializeValue: (obj) => { return obj.getPropertyValue("colSpan"); }, diff --git a/packages/survey-core/src/question_paneldynamic.ts b/packages/survey-core/src/question_paneldynamic.ts index d98e4355b6..791d51e497 100644 --- a/packages/survey-core/src/question_paneldynamic.ts +++ b/packages/survey-core/src/question_paneldynamic.ts @@ -345,6 +345,7 @@ export class QuestionPanelDynamicModel extends Question implements IDynamicItemM public getType(): string { return "paneldynamic"; } + protected get hasMinWidth(): boolean { return false; } protected getAllChildren(): Base[] { return [ ...super.getAllChildren(), @@ -2859,7 +2860,6 @@ Serializer.addClass( name: "templateDescription:text", serializationProperty: "locTemplateDescription", }, - { name: "minWidth", defaultFunc: () => "auto" }, { name: "noEntriesText:text", serializationProperty: "locNoEntriesText" }, { name: "allowAddPanel:boolean", default: true }, { name: "allowRemovePanel:boolean", default: true }, diff --git a/packages/survey-core/src/settings.ts b/packages/survey-core/src/settings.ts index 4853c0b911..cee7e65210 100644 --- a/packages/survey-core/src/settings.ts +++ b/packages/survey-core/src/settings.ts @@ -540,22 +540,6 @@ export var settings = { * @param callback A callback function that should be called with `true` if a user confirms an action or `false` otherwise. */ confirmActionAsync: <(message: string, callback: (res: boolean) => void, options?: IConfirmDialogOptions) => void>undefined, - /** - * A minimum width value for all survey elements. - * - * Default value: `"300px"` - * - * You can override this setting for individual elements: [`minWidth`](https://surveyjs.io/form-library/documentation/api-reference/surveyelement#minWidth). - */ - minWidth: "300px", - /** - * A maximum width value for all survey elements. - * - * Default value: `"100%"` - * - * You can override this setting for individual elements: [`maxWidth`](https://surveyjs.io/form-library/documentation/api-reference/surveyelement#maxWidth). - */ - maxWidth: "100%", /** * Specifies how many times surveys can re-evaluate expressions when a question value changes. This limit helps avoid recursions in expressions. * diff --git a/packages/survey-core/src/survey-element.ts b/packages/survey-core/src/survey-element.ts index 5630933a2e..2473d56a9f 100644 --- a/packages/survey-core/src/survey-element.ts +++ b/packages/survey-core/src/survey-element.ts @@ -1052,21 +1052,21 @@ export class SurveyElement extends SurveyElementCore implements ISurvey /** * Gets or sets minimum survey element width in CSS values. * - * Default value: "300px" (taken from [`settings.minWidth`](https://surveyjs.io/form-library/documentation/settings#minWidth)) + * Default value: "" (the element uses the theme's `--sjs-element-min-width` value) * @see maxWidth * @see renderWidth * @see width */ - @property() minWidth: string; + @property({ defaultValue: "" }) minWidth: string; /** * Gets or sets maximum survey element width in CSS values. * - * Default value: "100%" (taken from [`settings.maxWidth`](https://surveyjs.io/form-library/documentation/settings#maxWidth)) + * Default value: "" (the element width is not limited) * @see minWidth * @see renderWidth * @see width */ - @property() maxWidth: string; + @property({ defaultValue: "" }) maxWidth: string; /** * Returns a calculated width of the rendered survey element in CSS values. @@ -1122,38 +1122,74 @@ export class SurveyElement extends SurveyElementCore implements ISurvey private setRootStyle() { this.rootStyle = this.calcRootStyle(); } - protected calcRootStyle(): any { - const style: { [index: string]: any } = {}; - let _width; - if (!!this.parent) { - const columns = this.parent.getColumsForElement(this as any); - _width = columns.reduce((sum, col) => col.effectiveWidth + sum, 0); - if (!!_width && _width !== 100) { - style["flexGrow"] = 1; - style["flexShrink"] = 0; - style["flexBasis"] = _width + "%"; - style["minWidth"] = undefined; - style["maxWidth"] = this.maxWidth; + /** + * A total width, in percent, of the grid layout columns this element occupies, or 0 when the grid layout is disabled. + */ + private get gridColumnsWidth(): number { + if (!this.parent) return 0; + const columns = this.parent.getColumsForElement(this as any); + return columns.reduce((sum, col) => col.effectiveWidth + sum, 0); + } + /** + * Returns `true` if the element occupies a part of a grid layout row. Such elements are sized by their columns. + */ + public get isInGridColumn(): boolean { + const width = this.gridColumnsWidth; + return !!width && width !== 100; + } + /** + * Returns `false` for elements that shrink to their content instead of getting the default element min-width. + */ + protected get hasMinWidth(): boolean { return true; } + /** + * The default element min-width, which makes an element wrap onto its own row when the row gets too narrow, + * applies only to elements sized by the flex basis. Grid columns and panels shrink to their content instead. + * An element with its own minWidth replaces the default with an inline style. + */ + private get hasDefaultMinWidth(): boolean { + return this.hasMinWidth && !this.minWidth && !this.isInGridColumn && this.allowRootStyle && !!this.renderWidth; + } + public getWrapperCss(): string { + const css = this.survey?.getCss() || {}; + return new CssClassBuilder() + .append(css.elementWrapper) + .append(css.elementWrapperMinWidth, this.hasDefaultMinWidth) + .append(this.cssClasses.questionWrapper) + .toString(); + } + /** + * Returns the minWidth CSS value for an element that overrides the default min-width. It is scaled the same way as the theme's default. + */ + private calcMinWidth(): string { + if (!this.minWidth) return ""; + let minWidth = "" + this.minWidth; + if (minWidth === "auto") return ""; + if (minWidth.indexOf("px") > -1 && !!this.survey) { + const minWidthNum = parseFloat(minWidth.replace("px", "")); + if (!isNaN(minWidthNum)) { + minWidth = minWidthNum * (this.survey as any).widthScale / 100 + "px"; } } - if (Object.keys(style).length == 0) { - let minWidth: string | number = "" + this.minWidth; - if (!!minWidth && minWidth != "auto") { - if (minWidth.indexOf("px") != -1 && this.survey) { - minWidth = minWidth.replace("px", ""); - let minWidthNum = parseFloat(minWidth); - if (!isNaN(minWidthNum)) { - minWidth = minWidthNum * (this.survey as any).widthScale / 100; - minWidth = "" + minWidth + "px"; - } - } - minWidth = "min(100%, " + minWidth + ")"; - } - if (this.allowRootStyle && this.renderWidth) { - style["flexGrow"] = 1; - style["flexShrink"] = 1; - style["flexBasis"] = this.renderWidth; + return "min(100%, " + minWidth + ")"; + } + protected calcRootStyle(): any { + const style: { [index: string]: any } = {}; + const gridWidth = this.gridColumnsWidth; + if (!!gridWidth && gridWidth !== 100) { + style["flexGrow"] = 1; + style["flexShrink"] = 0; + style["flexBasis"] = gridWidth + "%"; + } else if (this.allowRootStyle && this.renderWidth) { + style["flexGrow"] = 1; + style["flexShrink"] = 1; + style["flexBasis"] = this.renderWidth; + } + if (Object.keys(style).length > 0) { + const minWidth = this.calcMinWidth(); + if (!!minWidth) { style["minWidth"] = minWidth; + } + if (!!this.maxWidth) { style["maxWidth"] = this.maxWidth; } } diff --git a/packages/survey-core/src/themes/adapters/bootstrap.scss b/packages/survey-core/src/themes/adapters/bootstrap.scss index 6e62b7809c..cc457caaf7 100644 --- a/packages/survey-core/src/themes/adapters/bootstrap.scss +++ b/packages/survey-core/src/themes/adapters/bootstrap.scss @@ -2,6 +2,9 @@ --sjs2-base-unit-radius: var(--bs-border-radius); --sjs2-base-unit-spacing: 0.25rem; + /* Let Bootstrap size the elements in a row instead of the default element min-width. */ + --sjs-element-min-width: 0; + --sjs2-color-utility-body: var(--bs-body-bg); --sjs2-color-utility-sheet: var(--bs-body-bg); --sjs2-color-utility-a11y: rgba(13, 110, 253, 0.25); @@ -345,11 +348,6 @@ height: calc(0.25rem * 2); } -/* Override settings.minWidth: https://surveyjs.io/form-library/documentation/api-reference/surveyelement#minWidth */ -.sd-row.sd-row--multiple > * { - min-width: 0 !important; -} - .sd-root-modern--mobile .sd-body__navigation .sd-action-bar__item:not(.sd-action-bar__item--hidden) { flex-grow: 0; } diff --git a/packages/survey-core/src/themes/adapters/mui.scss b/packages/survey-core/src/themes/adapters/mui.scss index 29b35107ca..c9f54b37f2 100644 --- a/packages/survey-core/src/themes/adapters/mui.scss +++ b/packages/survey-core/src/themes/adapters/mui.scss @@ -1,6 +1,9 @@ .sjs-theme-overrides { --sjs2-base-unit-radius: var(--mui-shape-borderRadius); --sjs2-base-unit-spacing: 8px; + + /* Let MUI size the elements in a row instead of the default element min-width. */ + --sjs-element-min-width: 0; --sjs2-color-utility-body: var(--mui-palette-background-default); --sjs2-color-utility-sheet: var(--mui-palette-background-default); --sjs2-border-effect-a11y: 0 0 0 2px var(--mui-palette-primary-main); @@ -728,11 +731,6 @@ input[type="range"].sd-slider__input::-webkit-slider-runnable-track { height: 4px; } -/* Override settings.minWidth: https://surveyjs.io/form-library/documentation/api-reference/surveyelement#minWidth */ -.sd-row.sd-row--multiple > * { - min-width: 0 !important; -} - .sd-root-modern--mobile .sd-body__navigation .sd-action-bar__item:not(.sd-action-bar__item--hidden) { flex-grow: 0; } diff --git a/packages/survey-core/src/themes/adapters/shadcn.scss b/packages/survey-core/src/themes/adapters/shadcn.scss index 44cffc4ab8..a8d45f1da0 100644 --- a/packages/survey-core/src/themes/adapters/shadcn.scss +++ b/packages/survey-core/src/themes/adapters/shadcn.scss @@ -26,6 +26,9 @@ --sjs2-base-unit-radius: var(--radius); --sjs2-base-unit-spacing: var(--spacing); + /* Let shadcn size the elements in a row instead of the default element min-width. */ + --sjs-element-min-width: 0; + --sjs2-color-utility-body: var(--card); --sjs2-color-utility-sheet: var(--background); --sjs2-color-utility-a11y: color-mix(in oklab, var(--ring) 50%, transparent); @@ -455,11 +458,6 @@ height: calc(var(--spacing) * 1.5); } -/* Override settings.minWidth: https://surveyjs.io/form-library/documentation/api-reference/surveyelement#minWidth */ -.sd-row.sd-row--multiple > * { - min-width: 0 !important; -} - .sd-root-modern--mobile .sd-body__navigation .sd-action-bar__item:not(.sd-action-bar__item--hidden) { flex-grow: 0; } diff --git a/packages/survey-core/tests/basetests.ts b/packages/survey-core/tests/basetests.ts index ef892a20c1..eadea46a21 100644 --- a/packages/survey-core/tests/basetests.ts +++ b/packages/survey-core/tests/basetests.ts @@ -803,16 +803,18 @@ describe("Base", () => { question.resetPropertyValue("width"); expect(question.width, "width property value is empty").toBeFalsy(); - expect(question.hasDefaultPropertyValue("minWidth"), "question.minWidth has default value").toBe(true); - expect(question.getDefaultPropertyValue("minWidth"), "question.minWidth default value is 300px").toBeTruthy(); - question.minWidth = "200px"; - expect(question.minWidth, "minWidth property is set to 200px").toBe("200px"); - question.resetPropertyValue("minWidth"); - expect(question.minWidth, "minWidth property value is reset, #1").toBe("300px"); - question.minWidth = ""; - expect(question.minWidth, "minWidth property value is empty string").toBe(""); - question.resetPropertyValue("minWidth"); - expect(question.minWidth, "minWidth property value is reset, #2").toBe("300px"); + Serializer.addProperty("dropdown", { name: "propWithDefaultFunc", defaultFunc: () => "300px" }); + expect(question.hasDefaultPropertyValue("propWithDefaultFunc"), "propWithDefaultFunc has default value").toBe(true); + expect(question.getDefaultPropertyValue("propWithDefaultFunc"), "propWithDefaultFunc default value is 300px").toBe("300px"); + question.setPropertyValue("propWithDefaultFunc", "200px"); + expect(question.getPropertyValue("propWithDefaultFunc"), "propWithDefaultFunc is set to 200px").toBe("200px"); + question.resetPropertyValue("propWithDefaultFunc"); + expect(question.getPropertyValue("propWithDefaultFunc"), "propWithDefaultFunc value is reset, #1").toBe("300px"); + question.setPropertyValue("propWithDefaultFunc", ""); + expect(question.getPropertyValue("propWithDefaultFunc"), "propWithDefaultFunc value is empty string").toBe(""); + question.resetPropertyValue("propWithDefaultFunc"); + expect(question.getPropertyValue("propWithDefaultFunc"), "propWithDefaultFunc value is reset, #2").toBe("300px"); + Serializer.removeProperty("dropdown", "propWithDefaultFunc"); expect(question.placeholder, "question.locPlaceholder default value").toBe("Select..."); expect(question.hasDefaultPropertyValue("placeholder"), "question.placeholder has default value").toBe(true); @@ -904,13 +906,19 @@ describe("Base", () => { survey.afterRerender(); expect(log).toBe(""); }); - test("maxWidth should return to default when set to empty string", () => { + test("onSettingValue: a property should return to default when set to empty string", () => { + Serializer.addProperty("dropdown", { + name: "propWithOnSettingValue", + defaultFunc: () => "100%", + onSettingValue: (obj: any, val: any): any => { return val || undefined; } + }); const question = new QuestionDropdownModel("q1"); - expect(question.maxWidth, "maxWidth default value is 100%").toBe("100%"); - question.maxWidth = "50%"; - expect(question.maxWidth, "maxWidth is set to 50%").toBe("50%"); - question.maxWidth = ""; - expect(question.maxWidth, "maxWidth returns to default on empty string").toBe("100%"); + expect(question.getPropertyValue("propWithOnSettingValue"), "default value is 100%").toBe("100%"); + question.setPropertyValue("propWithOnSettingValue", "50%"); + expect(question.getPropertyValue("propWithOnSettingValue"), "value is set to 50%").toBe("50%"); + question.setPropertyValue("propWithOnSettingValue", ""); + expect(question.getPropertyValue("propWithOnSettingValue"), "value returns to default on empty string").toBe("100%"); + Serializer.removeProperty("dropdown", "propWithOnSettingValue"); }); test("check default value doesn't exist on getPropertyValueDirectly", () => { diff --git a/packages/survey-core/tests/editingObjectTests.ts b/packages/survey-core/tests/editingObjectTests.ts index b64d066946..dd1c29e90f 100644 --- a/packages/survey-core/tests/editingObjectTests.ts +++ b/packages/survey-core/tests/editingObjectTests.ts @@ -1450,6 +1450,7 @@ describe("Survey.editingObj Tests", () => { const question = new QuestionMatrixDynamicModel("q1"); question.addColumn("col1"); const column = question.columns[0]; + column.minWidth = "150px"; var survey = new SurveyModel({ elements: [ { @@ -1461,43 +1462,44 @@ describe("Survey.editingObj Tests", () => { survey.editingObj = column; const editor = survey.getAllQuestions()[0]; const property = Serializer.findProperty(column.getType(), "minWidth"); - expect(editor.value).toBe("300px"); property.onPropertyEditorUpdate(column, editor); - expect(editor.value).toBe(""); + expect(editor.value, "the editor shows the column's own minWidth").toBe("150px"); }); test("Allow to set empty string into localization string & string property with default value", () => { + Serializer.addProperty("dropdown", { name: "propWithDefault", defaultFunc: () => "300px" }); const question = new QuestionDropdownModel("q1"); const survey = new SurveyModel({ elements: [ { type: "text", name: "placeholder" }, - { type: "text", name: "minWidth" }, + { type: "text", name: "propWithDefault" }, { type: "text", name: "valueName" }, ], }); survey.editingObj = question; const placeholderQuestion = survey.getQuestionByName("placeholder"); - const minWidthQuestion = survey.getQuestionByName("minWidth"); + const propQuestion = survey.getQuestionByName("propWithDefault"); const valueNameQuestion = survey.getQuestionByName("valueName"); expect(placeholderQuestion.value, "placeholder - default value").toBe("Select..."); - expect(minWidthQuestion.value, "minWidth - default value").toBe("300px"); + expect(propQuestion.value, "propWithDefault - default value").toBe("300px"); expect(valueNameQuestion.value, "valueName is empty by default").toBeFalsy(); placeholderQuestion.value = ""; - minWidthQuestion.value = ""; + propQuestion.value = ""; valueNameQuestion.value = ""; expect(placeholderQuestion.value, "placeholder question value is empty").toBe(""); - expect(minWidthQuestion.value, "minWidth question value is empty").toBe(""); + expect(propQuestion.value, "propWithDefault question value is empty").toBe(""); expect(question.placeholder, "dropdown.placeholder value is empty").toBe(""); - expect(question.minWidth, "dropdown.minWidth value is empty").toBe(""); + expect(question.getPropertyValue("propWithDefault"), "dropdown.propWithDefault value is empty").toBe(""); expect(question.toJSON()).toEqual({ - name: "q1", placeholder: "", minWidth: "" + name: "q1", placeholder: "", propWithDefault: "" }); const q2 = Serializer.createClass("dropdown"); q2.fromJSON({ - name: "q2", placeholder: "", minWidth: "" + name: "q2", placeholder: "", propWithDefault: "" }); expect(q2.name, "set the name correctly").toBe("q2"); expect(q2.placeholder, "q2.placeholder value is empty").toBe(""); - expect(q2.minWidth, "q2.minWidth value is empty").toBe(""); + expect(q2.getPropertyValue("propWithDefault"), "q2.propWithDefault value is empty").toBe(""); + Serializer.removeProperty("dropdown", "propWithDefault"); }); test("Edit triggers array", () => { const survey = new SurveyModel(); diff --git a/packages/survey-core/tests/layout_tests.ts b/packages/survey-core/tests/layout_tests.ts index b0a17274de..66115f776b 100644 --- a/packages/survey-core/tests/layout_tests.ts +++ b/packages/survey-core/tests/layout_tests.ts @@ -606,10 +606,11 @@ describe("Layout:", () => { expect(q1.rootStyle, "#1").toEqual({ "flexBasis": "100%", "flexGrow": 1, - "flexShrink": 1, - "maxWidth": "100%", - "minWidth": "min(100%, 300px)" + "flexShrink": 1 }); + expect(q1.isInGridColumn, "q1 spans the whole row").toBe(false); + expect(q1.getWrapperCss(), "q1 wrapper css") + .toBe("sd-element-wrapper sd-element-wrapper--min-width"); const q2 = new QuestionTextModel("q2"); q2.startWithNewLine = false; @@ -618,18 +619,19 @@ describe("Layout:", () => { expect(q1.rootStyle, "#2").toEqual({ "flexBasis": "50%", "flexGrow": 1, - "flexShrink": 0, - "maxWidth": "100%", - "minWidth": undefined + "flexShrink": 0 }); expect(q2.rootStyle, "#3").toEqual({ "flexBasis": "50%", "flexGrow": 1, - "flexShrink": 0, - "maxWidth": "100%", - "minWidth": undefined + "flexShrink": 0 }); + + // Elements sized by grid columns must not get the default element min-width from CSS. + expect(q1.isInGridColumn, "q1 is in a grid column").toBe(true); + expect(q1.getWrapperCss(), "q1 wrapper css").toBe("sd-element-wrapper"); + expect(q2.getWrapperCss(), "q2 wrapper css").toBe("sd-element-wrapper"); }); test("gridLayoutColumns: serialize last column", () => { diff --git a/packages/survey-core/tests/paneltests.ts b/packages/survey-core/tests/paneltests.ts index 976b18d2ed..3203ded91c 100644 --- a/packages/survey-core/tests/paneltests.ts +++ b/packages/survey-core/tests/paneltests.ts @@ -3745,19 +3745,6 @@ describe("Panel", () => { expect(panel.toJSON().name).toBe("panel1"); expect(panel.getPropertyValue("gridLayoutColumns"), "gridLayoutColumns array is not created on serializing").toBeUndefined(); }); - test("Panel maxWidth should return to default when set to empty string", () => { - const survey = new SurveyModel({ - elements: [ - { type: "panel", name: "panel1", elements: [{ type: "text", name: "q1" }] } - ] - }); - const panel = survey.getPanelByName("panel1"); - expect(panel.maxWidth, "panel maxWidth default value").toBe(settings.maxWidth); - panel.maxWidth = "50%"; - expect(panel.maxWidth, "panel maxWidth is set to 50%").toBe("50%"); - panel.maxWidth = ""; - expect(panel.maxWidth, "panel maxWidth returns to default on empty string").toBe(settings.maxWidth); - }); test("Required Validation Errors should not appear for Read-only Panels Bug#11136", () => { const survey = new SurveyModel({ elements: [ diff --git a/packages/survey-core/tests/surveyElementTests.ts b/packages/survey-core/tests/surveyElementTests.ts index 79f73f8673..e47b4dbd0d 100644 --- a/packages/survey-core/tests/surveyElementTests.ts +++ b/packages/survey-core/tests/surveyElementTests.ts @@ -220,14 +220,119 @@ describe("SurveyElement", () => { expect(q1.rootStyle).toEqual({ "flexBasis": "100%", "flexGrow": 1, - "flexShrink": 1, - "maxWidth": "100%", - "minWidth": "min(100%, 300px)", + "flexShrink": 1 }); q1.allowRootStyle = false; survey.css = defaultCss; expect(q1.rootStyle).toEqual({}); }); + test("getWrapperCss: only questions get the default element min-width", () => { + const survey = new SurveyModel({ + elements: [ + { type: "text", name: "q1" }, + { type: "paneldynamic", name: "q2" }, + { type: "panel", name: "panel1", elements: [{ type: "text", name: "q3" }] } + ] + }); + const q1 = survey.getQuestionByName("q1"); + expect(q1.getWrapperCss(), "a question") + .toBe("sd-element-wrapper sd-element-wrapper--min-width"); + expect(survey.getQuestionByName("q2").getWrapperCss(), "a dynamic panel shrinks to its content") + .toBe("sd-element-wrapper"); + expect(survey.getPanelByName("panel1").getWrapperCss(), "a panel shrinks to its content") + .toBe("sd-element-wrapper"); + + // An element the survey does not size gets no width constraints at all, as before. + q1.allowRootStyle = false; + expect(q1.rootStyle, "no root style").toEqual({}); + expect(q1.getWrapperCss(), "allowRootStyle is false").toBe("sd-element-wrapper"); + }); + test("getWrapperCss: an element can add its own wrapper class", () => { + const survey = new SurveyModel({ + elements: [{ type: "text", name: "q1" }] + }); + const q1 = survey.getQuestionByName("q1"); + q1.onUpdateCssClassesCallback = (css: any): void => { + css.questionWrapper = "custom-wrapper"; + }; + q1.updateElementCss(); + expect(q1.getWrapperCss()) + .toBe("sd-element-wrapper sd-element-wrapper--min-width custom-wrapper"); + }); + test("getWrapperCss: an element minWidth replaces the default one", () => { + const survey = new SurveyModel({ + elements: [ + { type: "text", name: "q1", minWidth: "200px" }, + { type: "text", name: "q2", minWidth: "auto" } + ] + }); + const q1 = survey.getQuestionByName("q1"); + expect(q1.getWrapperCss(), "q1 gets its minWidth from the root style") + .toBe("sd-element-wrapper"); + expect(q1.rootStyle.minWidth, "q1 root style").toBe("min(100%, 200px)"); + + const q2 = survey.getQuestionByName("q2"); + expect(q2.getWrapperCss(), "q2 shrinks to its content").toBe("sd-element-wrapper"); + expect(q2.rootStyle.minWidth, "q2 root style").toBeUndefined(); + + q2.minWidth = ""; + expect(q2.getWrapperCss(), "an empty minWidth falls back to the theme") + .toBe("sd-element-wrapper sd-element-wrapper--min-width"); + expect(q2.rootStyle.minWidth, "q2 root style is empty again").toBeUndefined(); + }); + test("minWidth & maxWidth override the default element widths", () => { + const survey = new SurveyModel({ + elements: [ + { type: "text", name: "q1" }, + { type: "text", name: "q2", minWidth: "200px", maxWidth: "400px" }, + { type: "panel", name: "panel1", minWidth: "auto", maxWidth: "500px", elements: [{ type: "text", name: "q3" }] } + ] + }); + const q1 = survey.getQuestionByName("q1"); + expect(q1.minWidth, "q1 minWidth is empty by default").toBe(""); + expect(q1.maxWidth, "q1 maxWidth is empty by default").toBe(""); + expect(q1.rootStyle, "q1 is sized by the theme").toEqual({ + "flexBasis": "100%", + "flexGrow": 1, + "flexShrink": 1 + }); + + const q2 = survey.getQuestionByName("q2"); + expect(q2.rootStyle, "q2 overrides the theme widths").toEqual({ + "flexBasis": "100%", + "flexGrow": 1, + "flexShrink": 1, + "minWidth": "min(100%, 200px)", + "maxWidth": "400px" + }); + + const panel = survey.getPanelByName("panel1"); + expect(panel.rootStyle, "the panel keeps shrinking to its content").toEqual({ + "flexBasis": "100%", + "flexGrow": 1, + "flexShrink": 1, + "maxWidth": "500px" + }); + + q2.maxWidth = ""; + expect(q2.rootStyle.maxWidth, "an empty maxWidth falls back to the theme").toBeUndefined(); + }); + test("minWidth & maxWidth do not fail on a number, Bug#: composite question", () => { + const survey = new SurveyModel({ + elements: [{ type: "text", name: "q1", minWidth: 100, maxWidth: 300 }] + }); + const q1 = survey.getQuestionByName("q1"); + // A unitless value produces an invalid CSS width that the browser ignores, as it did before. + expect(q1.rootStyle["minWidth"]).toBe("min(100%, 100)"); + expect(q1.rootStyle["maxWidth"]).toBe(300); + }); + test("minWidth is scaled the same way as the theme default", () => { + const survey = new SurveyModel({ + elements: [{ type: "text", name: "q1", minWidth: "300px" }] + }); + survey.widthScale = 200; + expect(survey.getQuestionByName("q1").rootStyle["minWidth"]).toBe("min(100%, 600px)"); + }); test("Do not create rootStyle by default", () => { const survey = new SurveyModel({ elements: [{ @@ -251,17 +356,13 @@ describe("SurveyElement", () => { expect(q1.rootStyle).toEqual({ "flexBasis": "100%", "flexGrow": 1, - "flexShrink": 1, - "maxWidth": "100%", - "minWidth": "min(100%, 300px)", + "flexShrink": 1 }); survey.setIsMobile(true); expect(q1.rootStyle).toEqual({ "flexBasis": "100%", "flexGrow": 1, - "flexShrink": 1, - "maxWidth": "100%", - "minWidth": "min(100%, 300px)" + "flexShrink": 1 }); }); test("question.errorLocation", () => { diff --git a/packages/survey-core/tests/surveyserializationtests.ts b/packages/survey-core/tests/surveyserializationtests.ts index 763f0099f8..1cc73a1143 100644 --- a/packages/survey-core/tests/surveyserializationtests.ts +++ b/packages/survey-core/tests/surveyserializationtests.ts @@ -612,16 +612,18 @@ describe("SurveySerialization", () => { expect(q1.placeholder, "ClearLocale for placeholder, #2").toBe("Select..."); }); test("Allow to save empty string for trings with default value", () => { + Serializer.addProperty("text", { name: "propWithDefault", defaultFunc: () => "300px" }); const q = new QuestionTextModel("q1"); - expect(q.minWidth, "Default value is 300px").toBe("300px"); - q.minWidth = ""; - expect(q.minWidth, "set empty width").toBe(""); + expect(q.getPropertyValue("propWithDefault"), "Default value is 300px").toBe("300px"); + q.setPropertyValue("propWithDefault", ""); + expect(q.getPropertyValue("propWithDefault"), "set empty value").toBe(""); const json = q.toJSON(); - expect(json, "Serialize empty minWidth").toEqual({ name: "q1", minWidth: "" }); - q.setPropertyValue("minWidth", undefined); - expect(q.minWidth, "Default value again").toBe("300px"); - q.fromJSON({ name: "q1", minWidth: "" }); - expect(q.minWidth, "empty width was in JSON").toBe(""); + expect(json, "Serialize empty propWithDefault").toEqual({ name: "q1", propWithDefault: "" }); + q.setPropertyValue("propWithDefault", undefined); + expect(q.getPropertyValue("propWithDefault"), "Default value again").toBe("300px"); + q.fromJSON({ name: "q1", propWithDefault: "" }); + expect(q.getPropertyValue("propWithDefault"), "empty value was in JSON").toBe(""); + Serializer.removeProperty("text", "propWithDefault"); }); test("An infinitive loop occurs at e.removePosFromObj Bug#8224", () => { ComponentCollection.Instance.add({ diff --git a/packages/survey-core/tests/surveytests.ts b/packages/survey-core/tests/surveytests.ts index 397260709a..6b3f5da942 100644 --- a/packages/survey-core/tests/surveytests.ts +++ b/packages/survey-core/tests/surveytests.ts @@ -16074,36 +16074,6 @@ describe("Survey", () => { survey.showProgressBar = "bottom"; expect(survey.getProgressCssClasses()).toBe("test_progress test_progress_bottom"); }); - test("settings.minWidth/maxWidth", () => { - const oldMinWidth = settings.minWidth; - const oldMaxWidth = settings.maxWidth; - settings.minWidth = "0px"; - settings.maxWidth = "500px"; - const survey = new SurveyModel({ - "showProgressBar": true, - "progressBarLocation": "top", - elements: [ - { type: "text", name: "q1" }, - { type: "text", name: "q2", minWidth: "50px" }, - { type: "text", name: "q3", maxWidth: "90%" }, - { type: "paneldynamic", name: "q4" }, - ], - }); - const q1 = survey.getQuestionByName("q1"); - const q2 = survey.getQuestionByName("q2"); - const q3 = survey.getQuestionByName("q3"); - const q4 = survey.getQuestionByName("q4"); - expect(q1.minWidth, "q1 minWidth").toBe("0px"); - expect(q1.maxWidth, "q1 maxWidth").toBe("500px"); - expect(q2.minWidth, "q2 minWidth").toBe("50px"); - expect(q2.maxWidth, "q2 maxWidth").toBe("500px"); - expect(q3.minWidth, "q3 minWidth").toBe("0px"); - expect(q3.maxWidth, "q3 maxWidth").toBe("90%"); - expect(q4.minWidth, "q4 (paneldynamic) minWidth").toBe("auto"); - settings.minWidth = oldMinWidth; - settings.maxWidth = oldMaxWidth; - }); - test("getContainerContent - navigation", () => { const json = { pages: [ diff --git a/packages/survey-core/tests/surveywidthmodetests.ts b/packages/survey-core/tests/surveywidthmodetests.ts index dc95b7f39f..23a9fcccc4 100644 --- a/packages/survey-core/tests/surveywidthmodetests.ts +++ b/packages/survey-core/tests/surveywidthmodetests.ts @@ -254,21 +254,4 @@ describe("Survey", () => { expect(survey.renderedWidth).toBe("900px"); }); - test("Question min width scaling", () => { - var survey = new SurveyModel({ - "elements": [ - { - "type": "text", - "name": "question1", - }, - ] - }); - const q = survey.getAllQuestions()[0]; - expect(survey.widthScale).toBe(100); - expect(q.rootStyle["minWidth"]).toBe("min(100%, 300px)"); - - survey.widthScale = 50; - expect(survey.widthScale).toBe(50); - expect(q.rootStyle["minWidth"]).toBe("min(100%, 150px)"); - }); }); diff --git a/packages/survey-react-ui/src/element.tsx b/packages/survey-react-ui/src/element.tsx index a6605e3573..a71bee9d2a 100644 --- a/packages/survey-react-ui/src/element.tsx +++ b/packages/survey-react-ui/src/element.tsx @@ -64,7 +64,6 @@ export class SurveyRowElement extends SurveyElementBase { protected renderElement(): React.JSX.Element { const element = this.element; const innerElement = this.createElement(element, this.index); - const css = (element as Question).cssClassesValue; const focusIn = () => { const el: any = element; if (el && el.isQuestion) { @@ -73,7 +72,7 @@ export class SurveyRowElement extends SurveyElementBase { }; return (
+
@@ -14,7 +14,7 @@
-
+
diff --git a/tests/markup/snapshots/layout-page-swnl-v2.snap.html b/tests/markup/snapshots/layout-page-swnl-v2.snap.html index 6a078b87cb..1b708c526b 100644 --- a/tests/markup/snapshots/layout-page-swnl-v2.snap.html +++ b/tests/markup/snapshots/layout-page-swnl-v2.snap.html @@ -1,11 +1,11 @@ -
+
-
+