Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion e2e/survey/surveyWidth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)");

Expand All @@ -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)");
});
});
});
2 changes: 1 addition & 1 deletion packages/survey-angular-ui/src/element.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<ng-template #template>
<div *ngIf="!!model" [class]="model.cssClasses.questionWrapper" [style]="rootStyle" (focusin)="model.focusIn()" #container>
<div *ngIf="!!model" [class]="model.getWrapperCss()" [style]="rootStyle" (focusin)="model.focusIn()" #container>
<ng-template [component]="{ name: componentName, data: componentData }"></ng-template>
</div>
</ng-template>
13 changes: 13 additions & 0 deletions packages/survey-core/src/default-theme/blocks/sd-element.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions packages/survey-core/src/default-theme/variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions packages/survey-core/src/defaultCss/defaultCss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions packages/survey-core/src/panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,7 @@ export class PanelModelBase extends SurveyElement<Question>
return classes;
}
protected getIdPrefix(): string { return "sp"; }
protected get hasMinWidth(): boolean { return false; }
public get isPanel(): boolean {
return false;
}
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/survey-core/src/question.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"); },
Expand Down
2 changes: 1 addition & 1 deletion packages/survey-core/src/question_paneldynamic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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 },
Expand Down
16 changes: 0 additions & 16 deletions packages/survey-core/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
102 changes: 69 additions & 33 deletions packages/survey-core/src/survey-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1052,21 +1052,21 @@ export class SurveyElement<E = any> 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.
Expand Down Expand Up @@ -1122,38 +1122,74 @@ export class SurveyElement<E = any> 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;
}
}
Expand Down
8 changes: 3 additions & 5 deletions packages/survey-core/src/themes/adapters/bootstrap.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
8 changes: 3 additions & 5 deletions packages/survey-core/src/themes/adapters/mui.scss
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
8 changes: 3 additions & 5 deletions packages/survey-core/src/themes/adapters/shadcn.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
40 changes: 24 additions & 16 deletions packages/survey-core/tests/basetests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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", () => {
Expand Down
Loading
Loading