Skip to content
Merged
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
3 changes: 2 additions & 1 deletion packages/survey-core/src/panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,8 +491,9 @@ export class PanelModelBase extends SurveyElement<Question>
const survey: any = this.survey;
const showInRuntime = survey && survey.showPageTitles && this.hasDescription;
if (this.isDesignMode) {
const hasEnteredDescription = survey && survey.showPageTitles && !!this.description;
let show = this.hasTitle &&
(showInRuntime && !!this.description ||
(hasEnteredDescription ||
(this.showDescription && settings.designMode.showEmptyDescriptions));
if (!!survey && !!survey.beforeShowInplaceDescriptionEditorCallback) {
show = survey.beforeShowInplaceDescriptionEditorCallback(this, !!show);
Expand Down
18 changes: 12 additions & 6 deletions packages/survey-core/src/survey-element.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { JsonObjectProperty, Serializer } from "./jsonobject";
import { property } from "./decorators";
import { Base } from "./base";
import { ArrayChanges, Base } from "./base";
import { EventBase } from "./event";
import { IAction } from "./actions/action";
import { AdaptiveActionContainer } from "./actions/adaptive-container";
Expand Down Expand Up @@ -101,15 +101,21 @@ export abstract class SurveyElementCore extends Base implements ILocalizableOwne
protected resetDescriptionVisibility(): void {
this.resetPropertyValue("hasDescription");
}
private updateDescriptionVisibility(): void {
if (this.getPropertyValueWithoutDefault("hasDescription") === undefined) return;
this.setPropertyValue("hasDescription", this.calcDescriptionVisibility());
}
protected propertyValueChanged(name: string, oldValue: any, newValue: any, arrayChanges?: ArrayChanges, target?: Base): void {
super.propertyValueChanged(name, oldValue, newValue, arrayChanges, target);
if (name === "description" || (name === "title" && this.isDesignMode)) {
this.updateDescriptionVisibility();
}
}
/**
* Explanatory text displayed under the title.
* @see hasDescription
*/
@property({
localizable: { markdown: true }, onSet: (newDescription, self) => {
self.resetDescriptionVisibility();
}
}) description: string;
@property({ localizable: { markdown: true } }) description: string;
get locDescription(): LocalizableString {
return this.getLocalizableString("description");
}
Expand Down
146 changes: 146 additions & 0 deletions packages/survey-core/tests/paneltests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,152 @@ describe("Panel", () => {

(<any>survey).beforeShowInplaceDescriptionEditorCallback = undefined;
});
test("PageModel: _showDescription passes the default visibility to the callback, not the callback's own result", () => {
const oldShowEmpty = settings.designMode.showEmptyDescriptions;
settings.designMode.showEmptyDescriptions = false;
const survey = new SurveyModel();
const page = survey.addNewPage("page");
page.title = "My page";
page.description = "My description";
survey.setDesignMode(true);
// A callback that hides the description. Its result must not become its own default afterwards.
const defaults: Array<boolean> = [];
(<any>survey).beforeShowInplaceDescriptionEditorCallback = (el: any, show: boolean): boolean => {
if (el === page) defaults.push(show);
return false;
};

expect(page._showDescription, "hidden by the callback").toBeFalsy();
defaults.length = 0;
expect(page._showDescription, "still hidden by the callback").toBeFalsy();
// The page has a description, so the default stays true however the callback voted before.
expect(defaults, "the callback gets the default visibility, not its previous result").toStrictEqual([true]);

(<any>survey).beforeShowInplaceDescriptionEditorCallback = undefined;
settings.designMode.showEmptyDescriptions = oldShowEmpty;
});
test("PanelModel: hasDescription is reset when the description is changed via its localizable string", () => {
const survey = new SurveyModel();
const page = survey.addNewPage("page");
const panel = page.addNewPanel("panel");
panel.title = "My panel";
survey.setDesignMode(true);
(<any>survey).beforeShowInplaceDescriptionEditorCallback = (el: any): boolean => !!el.description;

expect(panel.hasDescription, "empty -> hidden").toBeFalsy();
// The property grid and the in-place editor modify the localizable string directly.
panel.locDescription.text = "Hello";
expect(panel.hasDescription, "non-empty -> shown").toBeTruthy();
panel.locDescription.text = "";
expect(panel.hasDescription, "cleared -> hidden").toBeFalsy();

(<any>survey).beforeShowInplaceDescriptionEditorCallback = undefined;
});
test("PanelModel: hasDescription is reset when the title is changed, since the callback depends on it", () => {
const survey = new SurveyModel();
const page = survey.addNewPage("page");
const panel = page.addNewPanel("panel");
panel.description = "My description";
survey.setDesignMode(true);
(<any>survey).beforeShowInplaceDescriptionEditorCallback = (el: any): boolean => !!el.title && !!el.description;

expect(panel.hasDescription, "no title -> hidden").toBeFalsy();
panel.title = "My panel";
expect(panel.hasDescription, "title set -> shown").toBeTruthy();
panel.locTitle.text = "";
expect(panel.hasDescription, "title cleared -> hidden").toBeFalsy();

(<any>survey).beforeShowInplaceDescriptionEditorCallback = undefined;
});
test("PanelModel: beforeShowInplaceDescriptionEditorCallback is called on every title/description change", () => {
const survey = new SurveyModel();
const page = survey.addNewPage("page");
const panel = page.addNewPanel("panel");
survey.setDesignMode(true);
// The callback decides on both the title and the description.
const calls: Array<any> = [];
(<any>survey).beforeShowInplaceDescriptionEditorCallback = (el: any): boolean => {
const show = !!el.title && !!el.description;
if (el === panel) calls.push({ title: el.title, description: el.description, show: show });
return show;
};
// The first render calculates the visibility.
expect(panel.hasDescription, "no title, no description -> hidden").toBeFalsy();
calls.length = 0;

// Nothing below reads hasDescription: each change must call the callback by itself, otherwise
// the UI is never told to re-render the description.
panel.description = "My description";
expect(calls.length, "called after the description is set").toBe(1);
expect(calls[0], "the callback sees the new description").toStrictEqual(
{ title: "", description: "My description", show: false });

panel.title = "My panel";
expect(calls.length, "called after the title is set").toBe(2);
expect(calls[1], "the callback sees the new title").toStrictEqual(
{ title: "My panel", description: "My description", show: true });

panel.title = "";
expect(calls.length, "called after the title is cleared").toBe(3);
expect(calls[2], "the callback sees the cleared title").toStrictEqual(
{ title: "", description: "My description", show: false });

// The value is already calculated, so reading it calls the callback no more.
expect(panel.hasDescription, "no title -> hidden").toBeFalsy();
expect(calls.length, "reading hasDescription calls the callback no more").toBe(3);

(<any>survey).beforeShowInplaceDescriptionEditorCallback = undefined;
});
test("PanelModel: a title/description change notifies hasDescription with the new value", () => {
const survey = new SurveyModel();
const page = survey.addNewPage("page");
const panel = page.addNewPanel("panel");
panel.description = "My description";
survey.setDesignMode(true);
(<any>survey).beforeShowInplaceDescriptionEditorCallback = (el: any): boolean => !!el.title && !!el.description;

const notified: Array<any> = [];
panel.onPropertyChanged.add((_, opt) => { if (opt.name === "hasDescription") notified.push(opt.newValue); });

// The first render calculates the visibility.
expect(panel.hasDescription, "no title -> hidden").toBeFalsy();
expect(notified.length, "calculating the visibility notifies nothing").toBe(0);

// The UI re-renders on the notification, so it must carry the new visibility, not undefined.
panel.title = "My panel";
expect(notified, "hasDescription notified as true when the title is set").toStrictEqual([true]);

panel.title = "";
expect(notified, "hasDescription notified as false when the title is cleared").toStrictEqual([true, false]);

(<any>survey).beforeShowInplaceDescriptionEditorCallback = undefined;
});
test("PanelModel: a title change does not calculate the description visibility before it is rendered", () => {
const survey = new SurveyModel();
const page = survey.addNewPage("page");
const panel = page.addNewPanel("panel");
panel.description = "My description";
survey.setDesignMode(true);
let callCount = 0;
(<any>survey).beforeShowInplaceDescriptionEditorCallback = (el: any): boolean => {
if (el === panel) callCount++;
return !!el.title && !!el.description;
};

const notified: Array<string> = [];
panel.onPropertyChanged.add((_, opt) => notified.push(opt.name));

// hasDescription has never been requested, so nothing recalculates it and nothing is notified.
panel.title = "My panel";
expect(callCount, "the callback is not called for a panel that is not rendered yet").toBe(0);
expect(notified.indexOf("hasDescription"), "hasDescription is not notified").toBe(-1);

// It is calculated on the first read instead.
expect(panel.hasDescription, "title and description are set -> shown").toBeTruthy();
expect(callCount, "the callback is called on the first read").toBe(1);

(<any>survey).beforeShowInplaceDescriptionEditorCallback = undefined;
});

test("Page/Panel.getProgressInfo()", () => {
const page = new PageModel("q1");
Expand Down
63 changes: 63 additions & 0 deletions packages/survey-core/tests/surveyquestiontests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,69 @@ describe("Survey_Questions", () => {
expect(question1.hasDescriptionUnderTitle, "survey hidden - underTitle").toBe(false);
expect(question1.hasDescriptionUnderInput, "survey hidden - underInput").toBe(false);
});
test("Question: beforeShowInplaceDescriptionEditorCallback is called on every title/description change", () => {
const survey = new SurveyModel();
const page = survey.addNewPage("Page 1");
const question = new QuestionTextModel("q1");
page.addQuestion(question);
survey.setDesignMode(true);
// A question title falls back to the question name, so the callback checks the title
// localizable string to tell an explicitly titled question from an untitled one.
const calls: Array<any> = [];
(<any>survey).beforeShowInplaceDescriptionEditorCallback = (el: any): boolean => {
const show = !!el.locTitle.text && !!el.description;
if (el === question) calls.push({ title: el.locTitle.text, description: el.description, show: show });
return show;
};
// The first render calculates the visibility.
expect(question.hasDescriptionUnderTitle, "no title, no description -> hidden").toBe(false);
calls.length = 0;

// Nothing below reads the visibility: each change must call the callback by itself.
question.description = "My description";
expect(calls.length, "called after the description is set").toBe(1);
expect(calls[0], "the callback sees the new description").toStrictEqual(
{ title: "", description: "My description", show: false });

question.title = "My question";
expect(calls.length, "called after the title is set").toBe(2);
expect(calls[1], "the callback sees the new title").toStrictEqual(
{ title: "My question", description: "My description", show: true });
expect(question.hasDescriptionUnderTitle, "title set -> shown").toBe(true);

question.title = "";
expect(calls.length, "called after the title is cleared").toBe(3);
expect(calls[2], "the callback sees the cleared title").toStrictEqual(
{ title: "", description: "My description", show: false });
expect(question.hasDescriptionUnderTitle, "title cleared -> hidden").toBe(false);

(<any>survey).beforeShowInplaceDescriptionEditorCallback = undefined;
});
test("Question: hasDescription is recalculated when the title/description localizable string changes", () => {
// The property grid and the in-place editor modify the localizable strings directly, and the
// property setters are not called in this case.
const survey = new SurveyModel();
const page = survey.addNewPage("Page 1");
const question = new QuestionTextModel("q1");
page.addQuestion(question);
question.description = "My description";
survey.setDesignMode(true);
(<any>survey).beforeShowInplaceDescriptionEditorCallback = (el: any): boolean => !!el.locTitle.text && !!el.description;

expect(question.hasDescriptionUnderTitle, "no title -> hidden").toBe(false);
question.locTitle.text = "My question";
expect(question.hasDescriptionUnderTitle, "title set via locString -> shown").toBe(true);
question.locTitle.text = "";
expect(question.hasDescriptionUnderTitle, "title cleared via locString -> hidden").toBe(false);

question.locTitle.text = "My question";
question.locDescription.text = "";
expect(question.hasDescriptionUnderTitle, "description cleared via locString -> hidden").toBe(false);
question.locDescription.text = "My description";
expect(question.hasDescriptionUnderTitle, "description set via locString -> shown").toBe(true);

(<any>survey).beforeShowInplaceDescriptionEditorCallback = undefined;
});
test("Use value of checkbox question as an array", () => {
var survey = new SurveyModel();
var page = survey.addNewPage("Page 1");
Expand Down
Loading