From c754243fde42a504de00bb4dbfad29e9a84208f2 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 22 Apr 2024 14:11:05 +0300 Subject: [PATCH] Implement paneldynamic and matrixdynamic animations (#8126) * Refactor animation utils + Implement AnimationTab class * Implement animations in PanelDynamic * Implement matrixdynamic animations * Fix collapsed styles for matrix questions * Fix box-sizing for question's content * Fix padding animation is set incorrectly for complex questions * Fix separator animation causing scrollbar appear * Fix overflow while animating height * Fix angular and knockout builds * Fix scrolling during paneldynamic animations * Fix vr tests * Fix focus new panel doesnt work when animation is disabled * Fix styles * Fix vr test * small refactor * Add unit test on onNextRender method --- .../src/questions/matrix-row.component.html | 2 +- .../src/questions/matrix-row.component.ts | 21 +- .../src/questions/matrixtable.component.html | 32 +- .../src/questions/paneldynamic.component.html | 59 ++- .../src/questions/paneldynamic.component.ts | 9 +- packages/survey-vue3-ui/src/App.vue | 1 - packages/survey-vue3-ui/src/Element.vue | 3 +- packages/survey-vue3-ui/src/MatrixRow.vue | 49 +++ packages/survey-vue3-ui/src/MatrixTable.vue | 19 +- packages/survey-vue3-ui/src/PanelDynamic.vue | 54 ++- packages/survey-vue3-ui/src/index.ts | 2 + src/defaultCss/defaultV2Css.ts | 8 + src/defaultV2-theme/blocks/animations.scss | 39 +- src/defaultV2-theme/blocks/sd-element.scss | 42 +-- .../blocks/sd-matrixdynamic.scss | 103 +++++- .../blocks/sd-paneldynamic.scss | 129 ++++++- src/defaultV2-theme/blocks/sd-question.scss | 39 +- src/defaultV2-theme/blocks/sd-row.scss | 12 +- src/defaultV2-theme/blocks/sd-table.scss | 5 +- src/defaultV2-theme/variables.scss | 35 ++ src/knockout/koquestion_matrixdropdown.ts | 12 + .../templates/question-matrixdynamic.html | 2 +- .../templates/question-paneldynamic.html | 24 +- src/panel.ts | 4 +- src/question_matrixdropdownrendered.ts | 62 +++- src/question_paneldynamic.ts | 144 +++++++- src/question_ranking.ts | 2 +- src/react/components/matrix/row.tsx | 27 ++ .../reactquestion_matrixdropdownbase.tsx | 2 +- src/react/reactquestion_paneldynamic.tsx | 47 +-- src/survey-element.ts | 8 +- src/utils/animation.ts | 198 ++++++---- src/vue/paneldynamic.vue | 24 +- tests/components/popuptests.ts | 8 +- ...eldynamic-list-legacy-navigation.snap.html | 12 +- .../snapshots/paneldynamic-list.snap.html | 14 +- .../paneldynamic-no-footer-1.snap.html | 88 ++--- .../paneldynamic-no-footer-2.snap.html | 88 ++--- ...rogress-bottom-legacy-navigation.snap.html | 12 +- .../paneldynamic-progress-bottom.snap.html | 6 +- ...c-progress-top-legacy-navigation.snap.html | 12 +- .../paneldynamic-progress-top.snap.html | 6 +- ...neldynamic-remove-btn-action-bar.snap.html | 40 ++- .../paneldynamic-remove-btn-right.snap.html | 32 +- tests/question_matrixdynamictests.ts | 69 ++++ tests/question_paneldynamic_tests.ts | 145 ++++++++ tests/utilstests.ts | 340 +++++++++++++----- 47 files changed, 1530 insertions(+), 561 deletions(-) create mode 100644 packages/survey-vue3-ui/src/MatrixRow.vue diff --git a/packages/survey-angular-ui/src/questions/matrix-row.component.html b/packages/survey-angular-ui/src/questions/matrix-row.component.html index b0064408ef..7520487824 100644 --- a/packages/survey-angular-ui/src/questions/matrix-row.component.html +++ b/packages/survey-angular-ui/src/questions/matrix-row.component.html @@ -1,6 +1,6 @@ + [attr.data-sv-drop-target-matrix-row]="row && row.id" #container> diff --git a/packages/survey-angular-ui/src/questions/matrix-row.component.ts b/packages/survey-angular-ui/src/questions/matrix-row.component.ts index c33a308e4d..cba529521f 100644 --- a/packages/survey-angular-ui/src/questions/matrix-row.component.ts +++ b/packages/survey-angular-ui/src/questions/matrix-row.component.ts @@ -2,9 +2,7 @@ import { Component, ElementRef, Input, ViewChild } from "@angular/core"; import { BaseAngular } from "../base-angular"; import { MatrixDropdownRowModelBase, - Question, QuestionMatrixDropdownModelBase, - QuestionMatrixDropdownRenderedCell, QuestionMatrixDropdownRenderedRow } from "survey-core"; @@ -16,6 +14,7 @@ import { export class MatrixRowComponent extends BaseAngular { @Input() model!: QuestionMatrixDropdownRenderedRow; @Input() question!: QuestionMatrixDropdownModelBase; + @ViewChild("container", { static: false, read: ElementRef }) container!: ElementRef; protected getModel(): QuestionMatrixDropdownRenderedRow { return this.model; } @@ -25,4 +24,22 @@ export class MatrixRowComponent extends BaseAngular +
- - - - + + + + - + - + diff --git a/packages/survey-angular-ui/src/questions/paneldynamic.component.html b/packages/survey-angular-ui/src/questions/paneldynamic.component.html index 34d9d94bed..8dfa95dd78 100644 --- a/packages/survey-angular-ui/src/questions/paneldynamic.component.html +++ b/packages/survey-angular-ui/src/questions/paneldynamic.component.html @@ -3,39 +3,38 @@ -
-
+
+
- - - -
- - - - -
- -
-
- - + +
+ +
+ + + + +
+
+
+
+ +
-
+
-
+
@@ -48,13 +47,9 @@
-
+
- +
diff --git a/packages/survey-angular-ui/src/questions/paneldynamic.component.ts b/packages/survey-angular-ui/src/questions/paneldynamic.component.ts index 5d884b47a8..c8a034f98f 100644 --- a/packages/survey-angular-ui/src/questions/paneldynamic.component.ts +++ b/packages/survey-angular-ui/src/questions/paneldynamic.component.ts @@ -8,13 +8,8 @@ import { AngularComponentFactory } from "../component-factory"; templateUrl: "./paneldynamic.component.html" }) export class PanelDynamicQuestionComponent extends QuestionAngular implements OnInit { - get renderedPanels(): PanelModel[] { - if (this.model.isRenderModeList) return this.model.visiblePanels; - const panels = []; - if (this.model.currentPanel) { - panels.push(this.model.currentPanel); - } - return panels; + public trackPanelBy(_: number, panel: PanelModel) { + return panel.id; } protected override onModelChanged(): void { super.onModelChanged(); diff --git a/packages/survey-vue3-ui/src/App.vue b/packages/survey-vue3-ui/src/App.vue index 662dc54ea5..2cb0d5f3a6 100644 --- a/packages/survey-vue3-ui/src/App.vue +++ b/packages/survey-vue3-ui/src/App.vue @@ -2,7 +2,6 @@ import { defineComponent } from "vue"; import { SurveyModel, StylesManager } from "survey-core"; import Survey from "./Survey.vue"; -import "./index.css" const json = { "logoPosition": "right", diff --git a/packages/survey-vue3-ui/src/Element.vue b/packages/survey-vue3-ui/src/Element.vue index db8e32af59..4882f2d5a6 100644 --- a/packages/survey-vue3-ui/src/Element.vue +++ b/packages/survey-vue3-ui/src/Element.vue @@ -70,7 +70,7 @@ const componentData = computed(() => { }; }); -watch( +const stopWatch = watch( () => props.element, (newValue, oldValue) => { if (oldValue) { @@ -85,5 +85,6 @@ onMounted(() => { }); onUnmounted(() => { props.element.setWrapperElement(undefined); + stopWatch(); }); diff --git a/packages/survey-vue3-ui/src/MatrixRow.vue b/packages/survey-vue3-ui/src/MatrixRow.vue new file mode 100644 index 0000000000..a9930390cb --- /dev/null +++ b/packages/survey-vue3-ui/src/MatrixRow.vue @@ -0,0 +1,49 @@ + + diff --git a/packages/survey-vue3-ui/src/MatrixTable.vue b/packages/survey-vue3-ui/src/MatrixTable.vue index 659fa36d50..bc3e7ae9fb 100644 --- a/packages/survey-vue3-ui/src/MatrixTable.vue +++ b/packages/survey-vue3-ui/src/MatrixTable.vue @@ -36,22 +36,13 @@
diff --git a/packages/survey-vue3-ui/src/PanelDynamic.vue b/packages/survey-vue3-ui/src/PanelDynamic.vue index d25d022d8e..00b545ea25 100644 --- a/packages/survey-vue3-ui/src/PanelDynamic.vue +++ b/packages/survey-vue3-ui/src/PanelDynamic.vue @@ -27,29 +27,34 @@ v-if="getShowLegacyNavigation() && question.isProgressTopShowing" :question="question" /> - + { - if (props.question.isRenderModeList) return props.question.visiblePanels; - const panels = []; - if (props.question.currentPanel) { - panels.push(props.question.currentPanel); - } - return panels; -}); - const getShowLegacyNavigation = () => { return props.question["showLegacyNavigation"]; }; diff --git a/packages/survey-vue3-ui/src/index.ts b/packages/survey-vue3-ui/src/index.ts index 24871ac0dd..750b817be2 100644 --- a/packages/survey-vue3-ui/src/index.ts +++ b/packages/survey-vue3-ui/src/index.ts @@ -46,6 +46,7 @@ import MultipleText from "./Multipletext.vue"; import MultipletextItem from "./MultipletextItem.vue"; import Matrix from "./Matrix.vue"; +import MatrixRow from "./MatrixRow.vue"; import MatrixCell from "./MatrixCell.vue"; import MatrixDropdown from "./MatrixDropdown.vue"; import MatrixTable from "./MatrixTable.vue"; @@ -193,6 +194,7 @@ function registerComponents(app: App) { app.component("survey-multipletext-item", MultipletextItem); app.component("survey-matrix", Matrix); + app.component("survey-matrix-row", MatrixRow); app.component("survey-matrix-cell", MatrixCell); app.component("survey-matrixdropdown", MatrixDropdown); app.component("survey-matrixtable", MatrixTable); diff --git a/src/defaultCss/defaultV2Css.ts b/src/defaultCss/defaultV2Css.ts index 3d4bdb7d64..615df922d0 100644 --- a/src/defaultCss/defaultV2Css.ts +++ b/src/defaultCss/defaultV2Css.ts @@ -116,7 +116,11 @@ export var defaultV2Css = { footer: "sd-paneldynamic__footer", panelFooter: "sd-paneldynamic__panel-footer", footerButtonsContainer: "sd-paneldynamic__buttons-container", + panelsContainer: "sd-paneldynamic__panels-container", panelWrapperInRow: "sd-paneldynamic__panel-wrapper--in-row", + panelWrapperFadeIn: "sd-paneldynamic__panel-wrapper--fade-in", + panelWrapperFadeOut: "sd-paneldynamic__panel-wrapper--fade-out", + panelWrapperList: "sd-paneldynamic__panel-wrapper--list", progressBtnIcon: "icon-progressbuttonv2", noEntriesPlaceholder: "sd-paneldynamic__placeholder sd-question__placeholder", compact: "sd-element--with-frame sd-element--compact", @@ -505,6 +509,8 @@ export var defaultV2Css = { errorsCellBottom: "sd-table__cell--error-bottom", itemCell: "sd-table__cell--item", row: "sd-table__row", + rowFadeIn: "sd-table__row--fade-in", + rowFadeOut: "sd-table__row--fade-out", expandedRow: "sd-table__row--expanded", rowHasPanel: "sd-table__row--has-panel", rowHasEndActions: "sd-table__row--has-end-actions", @@ -543,6 +549,8 @@ export var defaultV2Css = { content: "sd-matrixdynamic__content sd-question__content", cell: "sd-table__cell", row: "sd-table__row", + rowFadeIn: "sd-table__row--fade-in", + rowFadeOut: "sd-table__row--fade-out", rowHasPanel: "sd-table__row--has-panel", rowHasEndActions: "sd-table__row--has-end-actions", expandedRow: "sd-table__row--expanded", diff --git a/src/defaultV2-theme/blocks/animations.scss b/src/defaultV2-theme/blocks/animations.scss index b084857758..5d67ed42f2 100644 --- a/src/defaultV2-theme/blocks/animations.scss +++ b/src/defaultV2-theme/blocks/animations.scss @@ -7,25 +7,46 @@ opacity: 1; } } - -@keyframes moveIn { +@keyframes changeHeight { from { - height: 0; + height: var(--animation-height-from); } - to { - height: var(--animation-height); + height: var(--animation-height-to); } } - -@keyframes overflowIn { - 0% { +@keyframes moveInWithOverflow { + from { overflow: hidden; + height: 0; } 99% { overflow: hidden; + height: var(--animation-height); } - 100% { + to { overflow: visible; + height: var(--animation-height); + } +} +@keyframes moveIn { + from { + height: 0; + } + + to { + height: var(--animation-height); + } +} + +@keyframes paddingFadeIn { + from { + padding-top: 0; + padding-bottom: 0; + } + + to { + padding-bottom: var(--animation-padding-bottom); + padding-top: var(--animation-padding-top); } } diff --git a/src/defaultV2-theme/blocks/sd-element.scss b/src/defaultV2-theme/blocks/sd-element.scss index 9af792ed3d..32bfd92f07 100644 --- a/src/defaultV2-theme/blocks/sd-element.scss +++ b/src/defaultV2-theme/blocks/sd-element.scss @@ -185,18 +185,6 @@ animation-delay: $element-move-out-delay, 0s; } -@keyframes paddingFadeIn { - from { - padding-top: 0; - padding-bottom: 0; - } - - to { - padding-bottom: var(--animation-padding-bottom); - padding-top: var(--animation-padding-top); - } -} - .sd-element__content { box-sizing: border-box; } @@ -207,25 +195,23 @@ } .sd-element__content--fade-in { - animation-name: fadeIn, moveIn, paddingFadeIn, overflowIn; + animation-name: fadeIn, moveInWithOverflow, paddingFadeIn; min-height: 0 !important; opacity: 0; animation-fill-mode: forwards; animation-timing-function: $ease-out; - animation-duration: $expand-fade-in-duration, $expand-move-in-duration, $expand-move-in-duration, - $expand-move-in-duration; - animation-delay: $expand-fade-in-delay, 0s, 0s, 0s; + animation-duration: $expand-fade-in-duration, $expand-move-in-duration, $expand-move-in-duration; + animation-delay: $expand-fade-in-delay, 0s, 0s; } .sd-element__content--fade-out { - animation-name: fadeIn, moveIn, paddingFadeIn, overflowIn; + animation-name: fadeIn, moveInWithOverflow, paddingFadeIn; min-height: 0 !important; animation-direction: reverse; animation-fill-mode: forwards; animation-timing-function: $reverse-ease-out; - animation-duration: $collapse-fade-out-duration, $collapse-move-out-duration, $collapse-move-out-duration, - $expand-move-in-duration; - animation-delay: 0s, $collapse-move-out-delay, $collapse-move-out-delay, $collapse-move-out-delay; + animation-duration: $collapse-fade-out-duration, $collapse-move-out-duration, $collapse-move-out-duration; + animation-delay: 0s, $collapse-move-out-delay, $collapse-move-out-delay; } .sd-element--expandable.sd-elemenet--expandable--animating { @@ -273,16 +259,26 @@ } } - &.sd-element--complex { + &.sd-element--complex:not(.sd-question--empty) { & > .sd-element__header--location-top:after { display: block; - animation-name: fadeIn; + opacity: 0; + height: 0; + --animation-height: 1px; + animation-name: fadeIn, moveIn; animation-fill-mode: forwards; - animation-duration: $transition-duration; + animation-timing-function: $ease-out; + animation-delay: $expand-fade-in-delay, 0s; + animation-duration: $expand-fade-in-duration, $expand-move-in-duration; } &.sd-element--collapsed .sd-element__header--location-top:after { animation-direction: reverse; + opacity: 1; + height: 1px; + animation-timing-function: $reverse-ease-out; + animation-delay: 0s, $collapse-move-out-delay; + animation-duration: $collapse-fade-out-duration, $collapse-move-out-duration; } } } diff --git a/src/defaultV2-theme/blocks/sd-matrixdynamic.scss b/src/defaultV2-theme/blocks/sd-matrixdynamic.scss index 50a0f75b12..1dca7c67bb 100644 --- a/src/defaultV2-theme/blocks/sd-matrixdynamic.scss +++ b/src/defaultV2-theme/blocks/sd-matrixdynamic.scss @@ -66,4 +66,105 @@ use { fill: $foreground-light; } -} \ No newline at end of file +} + +@keyframes borderAnimation { + from { + border-width: 0px; + } + to { + border-width: 8px; + } +} +@keyframes paddingAnimation { + from { + padding-top: 0; + padding-bottom: 0; + } + to { + padding-top: 24px; + padding-bottom: 32px; + } +} + +@keyframes empty { + from { + } + to { + } +} +.sd-table__row--fade-out, +.sd-table__row--fade-in { + animation-name: empty; + animation-fill-mode: forwards; + --fade-in-animation-duration: calc(#{$matrix-row-fade-in-duration} + #{$matrix-row-fade-in-delay}); + animation-duration: max(var(--fade-in-animation-duration), #{$matrix-row-move-in-duration}); + & > td { + animation-name: borderAnimation; + animation-timing-function: $ease-out; + animation-duration: $matrix-row-move-in-duration; + animation-fill-mode: forwards; + & > div { + animation-name: fadeIn, moveInWithOverflow; + opacity: 0; + animation-timing-function: $ease-out; + animation-fill-mode: forwards; + animation-duration: $matrix-row-fade-in-duration, $matrix-row-move-in-duration; + animation-delay: $matrix-row-fade-in-delay, 0s; + } + } +} +.sd-table__row--fade-out { + animation-direction: reverse; + --move-out-animation-duration: calc(#{$matrix-row-move-out-duration} + #{$matrix-row-move-out-delay}); + animation-duration: max(var(--move-out-animation-duration), #{$matrix-row-fade-out-duration}); + & > td { + animation-duration: $matrix-row-move-out-duration; + animation-delay: $matrix-row-move-out-delay; + animation-direction: reverse; + animation-timing-function: $reverse-ease-out; + & > div { + animation-direction: reverse; + animation-timing-function: $reverse-ease-out; + animation-duration: $matrix-row-fade-out-duration, $matrix-row-move-out-duration; + animation-delay: 0s, $matrix-row-move-out-delay; + } + } +} +.sd-table__row--detail { + &.sd-table__row--fade-in, + &.sd-table__row--fade-out { + & > td { + animation-name: borderAnimation, paddingAnimation; + animation-duration: $matrix-row-move-in-duration; + animation-fill-mode: forwards; + } + } + + &.sd-table__row--fade-in { + --fade-in-animation-duration: calc(#{$matrix-detail-row-fade-in-duration} + #{$matrix-detail-row-fade-in-delay}); + animation-duration: max(var(--fade-in-animation-duration), #{$matrix-detail-row-move-in-duration}); + & > td { + animation-timing-function: $ease-out; + animation-duration: $matrix-detail-row-move-in-duration; + & > div { + animation-duration: $matrix-detail-row-fade-in-duration, $matrix-detail-row-move-in-duration; + animation-delay: $matrix-detail-row-fade-in-delay, 0s; + } + } + } + &.sd-table__row--fade-out { + --move-out-animation-duration: calc(#{$matrix-detail-row-move-out-duration} + #{$matrix-detail-row-move-out-delay}); + animation-duration: max(var(--move-out-animation-duration), #{$matrix-detail-row-fade-out-duration}); + & > td { + animation-timing-function: $reverse-ease-out; + animation-duration: $matrix-detail-row-move-out-duration; + animation-delay: $matrix-detail-row-move-out-delay; + animation-direction: reverse; + & > div { + animation-duration: $matrix-detail-row-fade-out-duration, $matrix-detail-row-move-out-duration; + animation-delay: 0s, $matrix-detail-row-move-out-delay; + } + } + } +} diff --git a/src/defaultV2-theme/blocks/sd-paneldynamic.scss b/src/defaultV2-theme/blocks/sd-paneldynamic.scss index 19bef01c60..b4449beb1a 100644 --- a/src/defaultV2-theme/blocks/sd-paneldynamic.scss +++ b/src/defaultV2-theme/blocks/sd-paneldynamic.scss @@ -8,12 +8,12 @@ transform: translateY(-1px); } - &>.sd-panel { + & > .sd-panel { padding-top: 1px; padding-bottom: calc(0.5 * var(--sd-base-vertical-padding)); } - & .sd-paneldynamic__panel-wrapper>.sd-panel>.sd-panel__header { + & .sd-paneldynamic__panel-wrapper > .sd-panel > .sd-panel__header { padding-bottom: 0; &:after { @@ -22,7 +22,7 @@ padding-top: var(--sd-base-vertical-padding); - &>.sd-panel__title { + & > .sd-panel__title { color: $foreground-light; } } @@ -35,6 +35,22 @@ } } .sd-paneldynamic__separator { + display: none; +} +.sd-paneldynamic__panel-wrapper { + box-sizing: border-box; + padding-bottom: calc(1 * var(--sd-base-padding)); +} + +.sd-paneldynamic__panel-wrapper:after { + display: table; + clear: both; + content: " "; +} + +.sd-paneldynamic__footer .sd-paneldynamic__separator, +.sd-paneldynamic__panel-wrapper--list ~ .sd-paneldynamic__panel-wrapper--list:before { + content: " "; display: block; position: absolute; left: 0; @@ -45,21 +61,10 @@ height: 1px; border: none; } - .sd-paneldynamic__separator:only-child { display: none; } -.sd-paneldynamic__panel-wrapper { - padding-bottom: calc(1 * var(--sd-base-padding)); -} - -.sd-paneldynamic__panel-wrapper:after { - display: table; - clear: both; - content: " "; -} - .sd-paneldynamic__panel-wrapper--in-row { display: flex; flex-direction: row; @@ -135,14 +140,15 @@ } .sd-question--empty.sd-question--paneldynamic { - &>.sd-question__content { + & > .sd-question__content { padding-bottom: var(--sd-base-padding); + --animation-padding-bottom: var(--sd-base-padding); } } .sd-question--paneldynamic:not(.sd-question--empty) { - &>.sd-question__content { - &>.sd-question__comment-area { + & > .sd-question__content { + & > .sd-question__comment-area { padding-bottom: var(--sd-base-padding); } } @@ -267,7 +273,7 @@ align-items: center; } -.sd-question__title~.sd-tabs-toolbar { +.sd-question__title ~ .sd-tabs-toolbar { margin-top: calcSize(3); } @@ -281,4 +287,89 @@ .sd-question--paneldynamic.sd-element--with-frame { padding-bottom: 0; -} \ No newline at end of file +} + +.sd-paneldynamic__panels-container { + position: relative; + overflow: hidden; + margin-left: calc(-1 * var(--sd-base-padding)); + padding-left: var(--sd-base-padding); + margin-right: calc(-1 * var(--sd-base-padding)); + padding-right: var(--sd-base-padding); +} +.sd-paneldynamic__panel-wrapper { + box-sizing: border-box; +} + +@keyframes movePanel { + from { + transform: translateX(#{$pd-tab-move-margin}); + } + to { + transform: translateX(0); + } +} + +.sd-paneldynamic__panel-wrapper--fade-in-left, +.sd-paneldynamic__panel-wrapper--fade-in-right { + animation-name: movePanel, changeHeight, paddingFadeIn, fadeIn; + animation-duration: $pd-tab-move-in-duration, $pd-tab-height-change-duration, $pd-tab-height-change-duration, + $pd-tab-fade-in-duration; + animation-delay: $pd-tab-move-in-delay, $pd-tab-height-change-delay, $pd-tab-height-change-delay, + $pd-tab-fade-in-delay; + animation-timing-function: $ease-out; + animation-fill-mode: forwards; + opacity: 0; + padding-bottom: 0; + transform: translateX(#{$pd-tab-move-margin}); + height: var(--animation-height-from); + --animation-padding-bottom: calc(1 * var(--sd-base-padding)); +} +.sd-paneldynamic__panel-wrapper--fade-in-left { + --sjs-pd-tab-move-margin: calc(1 * #{$pd-tab-move-in-margin}); +} +.sd-paneldynamic__panel-wrapper--fade-in-right { + --sjs-pd-tab-move-margin: calc(-1 * #{$pd-tab-move-in-margin}); +} + +.sd-paneldynamic__panel-wrapper--fade-out-left, +.sd-paneldynamic__panel-wrapper--fade-out-right { + animation-name: fadeIn, movePanel; + animation-duration: $pd-tab-fade-out-duration, $pd-tab-move-out-duration; + animation-delay: $pd-tab-fade-out-delay, $pd-tab-move-out-delay; + animation-timing-function: $reverse-ease-out; + animation-direction: reverse; + animation-fill-mode: forwards; + position: absolute; + left: var(--sd-base-padding); + top: 0; + width: calc(100% - 2 * var(--sd-base-padding)); +} +.sd-paneldynamic__panel-wrapper--fade-out-left { + --sjs-pd-tab-move-margin: calc(-1 * #{$pd-tab-move-out-margin}); +} +.sd-paneldynamic__panel-wrapper--fade-out-right { + --sjs-pd-tab-move-margin: calc(1 * #{$pd-tab-move-out-margin}); +} + +.sd-paneldynamic__panel-wrapper--fade-in, +.sd-paneldynamic__panel-wrapper--fade-out { + animation-name: fadeIn, moveInWithOverflow, paddingFadeIn; + animation-fill-mode: forwards; + --animation-padding-bottom: calc(1 * var(--sd-base-padding)); + min-height: 0 !important; +} +.sd-paneldynamic__panel-wrapper--fade-in { + opacity: 0; + animation-timing-function: $ease-out; + animation-duration: $pd-list-fade-in-duration, $pd-list-move-in-duration, $pd-list-move-in-duration; + animation-delay: $pd-list-fade-in-delay, 0s, 0s; +} + +.sd-paneldynamic__panel-wrapper--fade-out { + animation-direction: reverse; + animation-timing-function: $reverse-ease-out; + animation-duration: $pd-list-fade-out-duration, $pd-list-move-out-duration, $pd-list-move-out-duration; + animation-delay: 0s, $pd-list-move-out-delay, $pd-list-move-out-delay; + --animation-padding-bottom: calc(1 * var(--sd-base-padding)); +} diff --git a/src/defaultV2-theme/blocks/sd-question.scss b/src/defaultV2-theme/blocks/sd-question.scss index 1c8c1f0d10..2b7f22c5ae 100644 --- a/src/defaultV2-theme/blocks/sd-question.scss +++ b/src/defaultV2-theme/blocks/sd-question.scss @@ -14,39 +14,39 @@ container-type: inline-size; } -.sd-question--title-top>.sd-question__erbox--above-question { +.sd-question--title-top > .sd-question__erbox--above-question { margin-bottom: calc(0.5 * var(--sd-base-vertical-padding)); } -.sd-question--description-under-input>.sd-question__erbox--below-question, -.sd-question--title-bottom>.sd-question__erbox--below-question { +.sd-question--description-under-input > .sd-question__erbox--below-question, +.sd-question--title-bottom > .sd-question__erbox--below-question { margin-top: calc(0.25 * var(--sd-base-vertical-padding) + 0.5 * #{$base-unit}); } -.sd-element--with-frame>.sd-element__erbox--above-element { +.sd-element--with-frame > .sd-element__erbox--above-element { margin-bottom: var(--sd-base-padding); border-radius: calcCornerRadius(1) calcCornerRadius(1) 0 0; } -.sd-question--left>.sd-element__erbox--above-element { +.sd-question--left > .sd-element__erbox--above-element { margin-bottom: 0; } -.sd-element--with-frame.sd-question--left>.sd-element__erbox--above-element { +.sd-element--with-frame.sd-question--left > .sd-element__erbox--above-element { margin-bottom: calc(1 * var(--sd-base-vertical-padding)); } -.sd-element--with-frame>.sd-question__erbox--below-question { +.sd-element--with-frame > .sd-question__erbox--below-question { margin-top: auto; border-radius: 0 0 calcCornerRadius(1) calcCornerRadius(1); } -.sd-element--with-frame.sd-question--title-top>.sd-question__erbox--above-question { +.sd-element--with-frame.sd-question--title-top > .sd-question__erbox--above-question { margin-bottom: calc(0.5 * var(--sd-base-vertical-padding) + #{$base-unit}); } -.sd-element--with-frame.sd-question--description-under-input>.sd-question__erbox--below-question, -.sd-element--with-frame.sd-question--title-bottom>.sd-question__erbox--below-question { +.sd-element--with-frame.sd-question--description-under-input > .sd-question__erbox--below-question, +.sd-element--with-frame.sd-question--title-bottom > .sd-question__erbox--below-question { margin-top: calc(0.5 * var(--sd-base-vertical-padding) + #{$base-unit}); } @@ -64,7 +64,7 @@ .sd-element--with-frame { &.sd-question--title-top { - padding-top: var(--sd-base-vertical-padding) + padding-top: var(--sd-base-vertical-padding); } &.sd-question--error-top { @@ -76,12 +76,12 @@ display: flex; flex-direction: column; - &>.sd-question__content { - margin-bottom: var(--sd-base-padding) + & > .sd-question__content { + margin-bottom: var(--sd-base-padding); } } - &>.sd-element__erbox { + & > .sd-element__erbox { margin-left: calc(-1 * var(--sd-base-padding)); margin-right: calc(-1 * var(--sd-base-padding)); width: calc(100% + 2 * var(--sd-base-padding)); @@ -114,7 +114,7 @@ margin-top: calc(0.25 * var(--sd-base-vertical-padding)) 0; } -.sd-element--with-frame>.sd-question__content--left { +.sd-element--with-frame > .sd-question__content--left { margin: 0; } @@ -146,8 +146,8 @@ line-height: multiply(1.5, $font-editorfont-size); color: $font-questionplaceholder-color; - &>div, - &>span { + & > div, + & > span { .sv-string-viewer { white-space: pre-line; } @@ -165,4 +165,7 @@ display: inline-block; height: multiply(1.5, $font-questiontitle-size); } -} \ No newline at end of file +} +.sd-question__content { + box-sizing: border-box; +} diff --git a/src/defaultV2-theme/blocks/sd-row.scss b/src/defaultV2-theme/blocks/sd-row.scss index fe888899e6..c0b06802ad 100644 --- a/src/defaultV2-theme/blocks/sd-row.scss +++ b/src/defaultV2-theme/blocks/sd-row.scss @@ -115,22 +115,22 @@ .sd-row--fade-in { animation-fill-mode: forwards; - animation-name: fadeIn, moveIn, marginFadeIn, overflowIn; + animation-name: fadeIn, moveInWithOverflow, marginFadeIn; min-height: 0 !important; opacity: 0; animation-timing-function: $ease-out; - animation-delay: $row-fade-in-delay, 0s, 0s, 0s; - animation-duration: $row-fade-in-duration, $row-move-in-duration, $row-move-in-duration, $row-move-in-duration; + animation-delay: $row-fade-in-delay, 0s, 0s; + animation-duration: $row-fade-in-duration, $row-move-in-duration, $row-move-in-duration; } .sd-row--fade-out { - animation-name: fadeIn, moveIn, marginFadeIn, overflowIn; + animation-name: fadeIn, moveInWithOverflow, marginFadeIn; animation-timing-function: $reverse-ease-out; animation-fill-mode: forwards; animation-direction: reverse; min-height: 0 !important; - animation-delay: 0s, $row-move-out-delay, $row-move-out-delay, $row-move-out-delay; - animation-duration: $row-fade-out-duration, $row-move-out-duration, $row-move-out-duration, $row-move-out-duration; + animation-delay: 0s, $row-move-out-delay, $row-move-out-delay; + animation-duration: $row-fade-out-duration, $row-move-out-duration, $row-move-out-duration; } .sd-row--fade-in .sd-element-wrapper--fade-in { diff --git a/src/defaultV2-theme/blocks/sd-table.scss b/src/defaultV2-theme/blocks/sd-table.scss index a04eb2552c..2570525385 100644 --- a/src/defaultV2-theme/blocks/sd-table.scss +++ b/src/defaultV2-theme/blocks/sd-table.scss @@ -58,6 +58,7 @@ &>.sd-question__header { &~.sd-question__content { padding-top: calcSize(2); + --animation-padding-top: #{calcSize(2)}; .sd-table--no-header { padding-top: calcSize(4); @@ -376,7 +377,7 @@ position: relative; overflow-x: auto; } - +.sd-question--table.sd-element--collapsed, .sd-question--table.sd-element--nested { overflow-x: visible; } @@ -450,6 +451,7 @@ &>.sd-question__content { padding-top: calcSize(2); + --animation-padding-top: #{calcSize(2)}; min-width: min-content; } @@ -486,6 +488,7 @@ .sd-question.sd-question--table { &>.sd-question__content { padding-top: 0; + --animation-padding-top: 0; } } diff --git a/src/defaultV2-theme/variables.scss b/src/defaultV2-theme/variables.scss index 9761726873..097f2cfeca 100644 --- a/src/defaultV2-theme/variables.scss +++ b/src/defaultV2-theme/variables.scss @@ -160,6 +160,41 @@ $element-fade-out-duration: var(--sjs-element-fade-out-duration, 150ms); $element-move-out-duration: var(--sjs-element-move-out-duration, 250ms); $element-move-out-delay: var(--sjs-element-move-out-delay, 0ms); +$matrix-row-fade-in-duration: var(--sjs-matrix-row-fade-in-duration, 250ms); +$matrix-row-move-in-duration: var(--sjs-matrix-row-move-in-duration, 150ms); +$matrix-row-fade-in-delay: var(--sjs-matrix-row-fade-in-delay, 150ms); +$matrix-row-fade-out-duration: var(--sjs-matrix-row-fade-out-duration, 100ms); +$matrix-row-move-out-duration: var(--sjs-matrix-row-move-out-duration, 250ms); +$matrix-row-move-out-delay: var(--sjs-matrix-row-move-out-delay, 100ms); + +$matrix-detail-row-fade-in-duration: var(--sjs--fade-in-duration, 500ms); +$matrix-detail-row-move-in-duration: var(--sjs-matrix-detail-row-move-in-duration, 150ms); +$matrix-detail-row-fade-in-delay: var(--sjs-matrix-detail-row-fade-in-delay, 150ms); +$matrix-detail-row-fade-out-duration: var(--sjs-matrix-detail-row-fade-out-duration, 150ms); +$matrix-detail-row-move-out-duration: var(--sjs-matrix-detail-row-move-out-duration, 250ms); +$matrix-detail-row-move-out-delay: var(--sjs-matrix-detail-row-move-out-delay, 100ms); + +$pd-tab-height-change-duration: var(--sjs-pd-tab-height-change-duration, 250ms); +$pd-tab-height-change-delay: var(--sjs-pd-tab-height-change-delay, 0ms); +$pd-tab-move-in-duration: var(--sjs-pd-tab-move-in-duration, 250ms); +$pd-tab-move-in-delay: var(--sjs-pd-tab-move-in-delay, 100ms); +$pd-tab-fade-in-duration: var(--sjs-pd-tab-fade-in-duration, 250ms); +$pd-tab-fade-in-delay: var(--sjs-pd-tab-fade-in-delay, 100ms); +$pd-tab-fade-out-duration: var(--sjs-pd-tab-fade-out-duration, 250ms); +$pd-tab-fade-out-delay: var(--sjs-pd-tab-fade-out-delay, 0ms); +$pd-tab-move-out-duration: var(--sjs-pd-tab-move-out-duration, 250ms); +$pd-tab-move-out-delay: var(--sjs-pd-tab-move-out-delay, 0ms); +$pd-tab-move-in-margin: var(--sjs-pd-tab-move-in-margin, 50%); +$pd-tab-move-out-margin: var(--sjs-pd-tab-move-out-margin, 50%); +$pd-tab-move-margin: var(--sjs-pd-tab-move-margin); + +$pd-list-fade-in-duration: var(--sjs-pd-list-fade-in-duration, 500ms); +$pd-list-move-in-duration: var(--sjs-pd-list-move-in-duration, 250ms); +$pd-list-fade-in-delay: var(--sjs-pd-list-fade-in-delay, 250ms); +$pd-list-fade-out-duration: var(--sjs-pd-list-fade-out-duration, 150ms); +$pd-list-move-out-duration: var(--sjs-pd-list-move-out-duration, 250ms); +$pd-list-move-out-delay: var(--sjs-pd-list-move-out-delay, 100ms); + $transition-duration: var(--sjs-transition-duration, 150ms); $ease-out: cubic-bezier(0, 0, 0.58, 1); diff --git a/src/knockout/koquestion_matrixdropdown.ts b/src/knockout/koquestion_matrixdropdown.ts index 7f3ed926ab..c2c6dd914b 100644 --- a/src/knockout/koquestion_matrixdropdown.ts +++ b/src/knockout/koquestion_matrixdropdown.ts @@ -55,6 +55,18 @@ export class QuestionMatrixBaseImplementor extends QuestionImplementor { this.setCallbackFunc("koPanelAfterRender", (el: any, con: any) => { this.panelAfterRender(el, con); }); + this.setCallbackFunc("koRowAfterRender", (htmlElements: any, element: QuestionMatrixDropdownRenderedRow) => { + for (var i = 0; i < htmlElements.length; i++) { + var tEl = htmlElements[i]; + var nName = tEl.nodeName; + if(nName !== "#text" && nName !== "#comment") { + element.setRootElement(tEl); + ko.utils.domNodeDisposal.addDisposeCallback(tEl, () => { + element.setRootElement(undefined); + }); + } + } + }); } public get matrix(): QuestionMatrixDropdownModel { return this.question; } private cellAfterRender(elements: any, con: any) { diff --git a/src/knockout/templates/question-matrixdynamic.html b/src/knockout/templates/question-matrixdynamic.html index fac155152e..1cf9e13c92 100644 --- a/src/knockout/templates/question-matrixdynamic.html +++ b/src/knockout/templates/question-matrixdynamic.html @@ -118,7 +118,7 @@ - + diff --git a/src/knockout/templates/question-paneldynamic.html b/src/knockout/templates/question-paneldynamic.html index 0f689ee2b0..ec565d37fe 100644 --- a/src/knockout/templates/question-paneldynamic.html +++ b/src/knockout/templates/question-paneldynamic.html @@ -10,7 +10,8 @@ - +
+
@@ -26,6 +27,7 @@
+
@@ -44,15 +46,19 @@ -
- - - - - - +
+ +
+ + + - + + + + +
+
diff --git a/src/panel.ts b/src/panel.ts index 08bae73884..2a67c558c5 100644 --- a/src/panel.ts +++ b/src/panel.ts @@ -121,7 +121,7 @@ export class QuestionRowModel extends Base { el.style.setProperty("--animation-width", getElementWidth(el) + "px"); }; return { - isAnimationEnabled: () => settings.animationEnabled && this.panel?.animationAllowed && this.visible, + isAnimationEnabled: () => this.panel?.animationAllowed && this.visible, getAnimatedElement: (element: IElement) => (element as any as SurveyElement).getWrapperElement(), getLeaveOptions: (element: IElement) => { const surveyElement = element as unknown as SurveyElement; @@ -305,7 +305,7 @@ export class PanelModelBase extends SurveyElement el.style.setProperty("--animation-height", el.offsetHeight + "px"); }; return { - isAnimationEnabled: () => settings.animationEnabled && this.animationAllowed, + isAnimationEnabled: () => this.animationAllowed, getAnimatedElement: (row: QuestionRowModel) => row.getRootElement(), getLeaveOptions: (_: QuestionRowModel) => { return { cssClass: this.cssClasses.rowFadeOut, diff --git a/src/question_matrixdropdownrendered.ts b/src/question_matrixdropdownrendered.ts index 47eeb903e2..d2d159c108 100644 --- a/src/question_matrixdropdownrendered.ts +++ b/src/question_matrixdropdownrendered.ts @@ -1,8 +1,7 @@ import { property, propertyArray } from "./jsonobject"; import { Question } from "./question"; -import { Base, ComputedUpdater } from "./base"; +import { Base } from "./base"; import { ItemValue } from "./itemvalue"; -import { surveyLocalization } from "./surveyStrings"; import { LocalizableString } from "./localizablestring"; import { PanelModel } from "./panel"; import { Action, IAction } from "./actions/action"; @@ -13,6 +12,7 @@ import { MatrixDropdownCell, MatrixDropdownRowModelBase, QuestionMatrixDropdownM import { ActionContainer } from "./actions/container"; import { QuestionMatrixDynamicModel } from "./question_matrixdynamic"; import { settings } from "./settings"; +import { AnimationGroup, IAnimationConsumer } from "./utils/animation"; export class QuestionMatrixDropdownRenderedCell { private static counter = 1; @@ -148,6 +148,7 @@ export class QuestionMatrixDropdownRenderedRow extends Base { @property({ defaultValue: false }) isGhostRow: boolean; @property({ defaultValue: false }) isAdditionalClasses: boolean; @property({ defaultValue: true }) visible: boolean; + public onVisibilityChangedCallback: () => void; public hasEndActions: boolean = false; public row: MatrixDropdownRowModelBase; public isErrorsRow = false; @@ -177,6 +178,13 @@ export class QuestionMatrixDropdownRenderedRow extends Base { .append(this.cssClasses.rowAdditional, this.isAdditionalClasses) .toString(); } + private rootElement: HTMLTableRowElement; + public setRootElement(val: HTMLTableRowElement): void { + this.rootElement = val; + } + public getRootElement(): HTMLTableRowElement { + return this.rootElement; + } } export class QuestionMatrixDropdownRenderedErrorRow extends QuestionMatrixDropdownRenderedRow { public isErrorsRow: boolean = true; @@ -216,8 +224,56 @@ export class QuestionMatrixDropdownRenderedTable extends Base { @propertyArray({ onPush: (_: any, i: number, target: QuestionMatrixDropdownRenderedTable) => { target.renderedRowsChangedCallback(); + target.updateRenderedRows(); }, + onRemove: (_: any, i: number, target: QuestionMatrixDropdownRenderedTable) => { + target.updateRenderedRows(); + } }) rows: Array; + private _animationAllowed: boolean = true; + private get animationAllowed(): boolean { + return this._animationAllowed && this.matrix.animationAllowed; + } + private set animationAllowed(val: boolean) { + this._animationAllowed = val; + } + private getRenderedRowsAnimationOptions(): IAnimationConsumer<[QuestionMatrixDropdownRenderedRow]> { + const beforeAnimationRun = (el: HTMLElement) => { + el.querySelectorAll(":scope > td > *").forEach((el:HTMLElement) => { + el.style.setProperty("--animation-height", el.offsetHeight + "px"); + }); + }; + return { + isAnimationEnabled: () => { + return this.animationAllowed; + }, + getAnimatedElement(el: QuestionMatrixDropdownRenderedRow) { + return el.getRootElement(); + }, + getLeaveOptions: () => { + return { cssClass: this.cssClasses.rowFadeOut, onBeforeRunAnimation: beforeAnimationRun }; + }, + getEnterOptions: () => { + return { cssClass: this.cssClasses.rowFadeIn, onBeforeRunAnimation: beforeAnimationRun }; + } + }; + } + + @propertyArray() private _renderedRows: Array = []; + public updateRenderedRows(): void { + this.renderedRows = this.rows; + } + private renderedRowsAnimation = new AnimationGroup(this.getRenderedRowsAnimationOptions(), (val) => { + this._renderedRows = val; + this.renderedRowsChangedCallback(); + }, () => this._renderedRows) + + public get renderedRows(): Array { + return this._renderedRows; + } + public set renderedRows(val: Array) { + this.renderedRowsAnimation.sync(val); + } public constructor(public matrix: QuestionMatrixDropdownModelBase) { super(); @@ -485,10 +541,12 @@ export class QuestionMatrixDropdownRenderedTable extends Base { } } protected buildRows() { + this.animationAllowed = false; var rows = this.matrix.isColumnLayoutHorizontal ? this.buildHorizontalRows() : this.buildVerticalRows(); this.rows = rows; + this.animationAllowed = true; } private hasActionCellInRowsValues: any = {}; private hasActionCellInRows(location: "start" | "end"): boolean { diff --git a/src/question_paneldynamic.ts b/src/question_paneldynamic.ts index 3a8d69286d..319900a66a 100644 --- a/src/question_paneldynamic.ts +++ b/src/question_paneldynamic.ts @@ -18,11 +18,11 @@ import { } from "./textPreProcessor"; import { Question, IConditionObject, IQuestionPlainData } from "./question"; import { PanelModel } from "./panel"; -import { JsonObject, property, Serializer } from "./jsonobject"; +import { JsonObject, property, propertyArray, Serializer } from "./jsonobject"; import { QuestionFactory } from "./questionfactory"; import { KeyDuplicationError } from "./error"; import { settings } from "./settings"; -import { confirmActionAsync } from "./utils/utils"; +import { classesToSelector, confirmActionAsync } from "./utils/utils"; import { SurveyError } from "./survey-error"; import { CssClassBuilder } from "./utils/cssClassBuilder"; import { ActionContainer } from "./actions/container"; @@ -30,6 +30,7 @@ import { Action, IAction } from "./actions/action"; import { ComputedUpdater } from "./base"; import { AdaptiveActionContainer } from "./actions/adaptive-container"; import { ITheme } from "./themes"; +import { AnimationGroup, AnimationProperty, AnimationTab, IAnimationConsumer } from "./utils/animation"; export interface IQuestionPanelDynamicData { getItemIndex(item: ISurveyData): number; @@ -505,6 +506,7 @@ export class QuestionPanelDynamicModel extends Question if(!this.currentPanel) { this.currentPanel = panel; } + this.updateRenderedPanels(); } private onPanelRemoved(panel: PanelModel): void { let index = this.onPanelRemovedCore(panel); @@ -513,6 +515,7 @@ export class QuestionPanelDynamicModel extends Question if(index >= visPanels.length) index = visPanels.length - 1; this.currentPanel = index >= 0 ? visPanels[index] : null; } + this.updateRenderedPanels(); } private onPanelRemovedCore(panel: PanelModel): number { const visPanels = this.visiblePanelsCore; @@ -570,6 +573,7 @@ export class QuestionPanelDynamicModel extends Question curPanel.onHidingContent(); } this.setPropertyValue("currentPanel", val); + this.updateRenderedPanels(); this.updateFooterActions(); this.updateTabToolbarItemsPressedState(); this.fireCallback(this.currentIndexChangedCallback); @@ -581,6 +585,110 @@ export class QuestionPanelDynamicModel extends Question this.survey.dynamicPanelCurrentIndexChanged(this, options); } } + + @propertyArray({ }) private _renderedPanels: Array = []; + + private updateRenderedPanels() { + if(this.isRenderModeList) { + this.renderedPanels = [].concat(this.visiblePanels); + } else if(this.currentPanel) { + this.renderedPanels = [this.currentPanel]; + } else { + this.renderedPanels = []; + } + } + + public set renderedPanels(val: Array) { + if(this.renderedPanels.length == 0 || val.length == 0) { + this._renderedPanels = val; + } else { + this.isPanelsAnimationRunning = true; + this.panelsAnimation.sync(val); + } + } + + public get renderedPanels(): Array { + return this._renderedPanels; + } + private isPanelsAnimationRunning: boolean = false; + private getPanelsAnimationOptions(): IAnimationConsumer<[PanelModel]> { + const getDirection = () => { + if(this.isRenderModeList) return ""; + const leavingPanel = this.renderedPanels.filter(el => el !== this.currentPanel)[0]; + let leavingPanelIndex = this.visiblePanels.indexOf(leavingPanel); + if(leavingPanelIndex < 0) leavingPanelIndex = this.removedPanelIndex; + return leavingPanelIndex > this.currentIndex ? "-right" : "-left"; + }; + return { + getAnimatedElement: (panel) => { + if(panel && this.cssContent) { + const contentSelector = classesToSelector(this.cssContent); + return this.getWrapperElement()?.querySelector(`${contentSelector} #${panel.id}`)?.parentElement; + } + }, + getEnterOptions: () => { + const cssClass = this.cssClasses.panelWrapperFadeIn ? `${this.cssClasses.panelWrapperFadeIn}${getDirection()}` : ""; + return { + onBeforeRunAnimation: (el) => { + if(this.focusNewPanelCallback) { + const scolledElement = this.isRenderModeList ? el : el.parentElement; + SurveyElement.ScrollElementToViewCore(scolledElement, false, false, { behavior: "smooth" }); + } + if(!this.isRenderModeList) { + el.parentElement?.style.setProperty("--animation-height-to", el.offsetHeight + "px"); + } else { + el.style.setProperty("--animation-height", el.offsetHeight + "px"); + } + }, + cssClass: cssClass + }; + }, + getLeaveOptions: () => { + const cssClass = this.cssClasses.panelWrapperFadeOut ? `${this.cssClasses.panelWrapperFadeOut}${getDirection()}` : ""; + return { + onBeforeRunAnimation: (el) => { + if(!this.isRenderModeList) { + el.parentElement?.style.setProperty("--animation-height-from", el.offsetHeight + "px"); + } else { + el.style.setProperty("--animation-height", el.offsetHeight + "px"); + } + }, + cssClass: cssClass }; + }, + isAnimationEnabled: () => { + return this.animationAllowed && !!this.getWrapperElement(); + }, + }; + } + + private _panelsAnimations: AnimationProperty, [PanelModel]>; + private disablePanelsAnimations() { + this.panelsCore.forEach((panel) => { + panel.animationAllowed = false; + }); + } + private enablePanelsAnimations() { + this.panelsCore.forEach((panel) => { + panel.animationAllowed = true; + }); + } + private updatePanelsAnimation() { + this._panelsAnimations = new (this.isRenderModeList ? AnimationGroup : AnimationTab)(this.getPanelsAnimationOptions(), (val, isTempUpdate?: boolean) => { + this._renderedPanels = val; + if(!isTempUpdate) { + this.isPanelsAnimationRunning = false; + this.focusNewPanel(); + } + }, () => this._renderedPanels); + } + + get panelsAnimation(): AnimationProperty, [PanelModel]> { + if(!this._panelsAnimations) { + this.updatePanelsAnimation(); + } + return this._panelsAnimations; + } + public onHidingContent(): void { super.onHidingContent(); if(this.currentPanel) { @@ -811,11 +919,13 @@ export class QuestionPanelDynamicModel extends Question if (val < this.panelCount) { this.panelsCore.splice(val, this.panelCount - val); } + this.disablePanelsAnimations(); this.setValueAfterPanelsCreating(); this.setValueBasedOnPanelCount(); this.reRunCondition(); this.updateFooterActions(); this.fireCallback(this.panelCountChangedCallback); + this.enablePanelsAnimations(); } /** * Returns the number of visible panels in Dynamic Panel. @@ -1048,6 +1158,10 @@ export class QuestionPanelDynamicModel extends Question public set renderMode(val: string) { this.setPropertyValue("renderMode", val); this.fireCallback(this.renderModeChangedCallback); + this.animationAllowed = false; + this.updateRenderedPanels(); + this.animationAllowed = true; + this.updatePanelsAnimation(); } public get tabAlign(): "center" | "left" | "right" { return this.getPropertyValue("tabAlign"); @@ -1249,9 +1363,22 @@ export class QuestionPanelDynamicModel extends Question if (this.renderMode === "list" && this.panelsState !== "default") { newPanel.expand(); } - newPanel.focusFirstQuestion(); + this.focusNewPanelCallback = () => { + newPanel.focusFirstQuestion(); + }; + if(!this.isPanelsAnimationRunning) { + this.focusNewPanel(); + } return newPanel; } + private focusNewPanelCallback: () => void; + private focusNewPanel() { + if(this.focusNewPanelCallback) { + this.focusNewPanelCallback(); + this.focusNewPanelCallback = undefined; + } + } + /** * Adds a new panel based on the [template](https://surveyjs.io/form-library/documentation/api-reference/dynamic-panel-model#template). * @param index *(Optional)* An index at which to insert the new panel. `undefined` adds the panel to the end or inserts it after the current panel if [`renderMode`](https://surveyjs.io/form-library/documentation/api-reference/dynamic-panel-model#renderMode) is `"tab"`. A negative index (for instance, -1) adds the panel to the end in all cases, regardless of the `renderMode` value. @@ -1356,9 +1483,11 @@ export class QuestionPanelDynamicModel extends Question * @see panels * @see template */ + private removedPanelIndex: number; public removePanel(value: any): void { const visIndex = this.getVisualPanelIndex(value); if (visIndex < 0 || visIndex >= this.visiblePanelCount) return; + this.removedPanelIndex = visIndex; const panel = this.visiblePanelsCore[visIndex]; const index = this.panelsCore.indexOf(panel); if(index < 0) return; @@ -1549,6 +1678,7 @@ export class QuestionPanelDynamicModel extends Question private buildPanelsFirstTime(force: boolean = false): void { if(this.hasPanelBuildFirstTime) return; if(!force && this.wasNotRenderedInSurvey) return; + this.animationAllowed = false; this.hasPanelBuildFirstTime = true; this.isBuildingPanelsFirstTime = true; if (this.getPropertyValue("panelCount") > 0) { @@ -1571,6 +1701,7 @@ export class QuestionPanelDynamicModel extends Question } this.updateFooterActions(); this.isBuildingPanelsFirstTime = false; + this.animationAllowed = true; } private get wasNotRenderedInSurvey(): boolean { return !this.hasPanelBuildFirstTime && !this.wasRendered && !!this.survey; @@ -2093,7 +2224,7 @@ export class QuestionPanelDynamicModel extends Question return new CssClassBuilder().append(super.getRootCss()).append(this.cssClasses.empty, this.getShowNoEntriesPlaceholder()).toString(); } public get cssHeader(): string { - const showTab = this.isRenderModeTab && !!this.panelCount; + const showTab = this.isRenderModeTab && !!this.visiblePanelCount; return new CssClassBuilder() .append(this.cssClasses.header) .append(this.cssClasses.headerTop, this.hasTitleOnTop || showTab) @@ -2103,6 +2234,7 @@ export class QuestionPanelDynamicModel extends Question public getPanelWrapperCss(panel: PanelModel): string { return new CssClassBuilder() .append(this.cssClasses.panelWrapper, !panel || panel.visible) + .append(this.cssClasses.panelWrapperList, this.isRenderModeList) .append(this.cssClasses.panelWrapperInRow, this.panelRemoveButtonLocation === "right") .toString(); } @@ -2319,13 +2451,15 @@ export class QuestionPanelDynamicModel extends Question get showLegacyNavigation(): boolean { return !this.isDefaultV2Theme; } + get showNavigation(): boolean { if (this.isReadOnly && this.visiblePanelCount == 1) return false; return this.visiblePanelCount > 0 && !this.showLegacyNavigation && !!this.cssClasses.footer; } showSeparator(index: number): boolean { - return this.isRenderModeList && index < this.visiblePanelCount - 1; + return this.isRenderModeList && index < this.renderedPanels.length - 1; } + protected calcCssClasses(css: any): any { const classes = super.calcCssClasses(css); const additionalTitleToolbar = this.additionalTitleToolbar; diff --git a/src/question_ranking.ts b/src/question_ranking.ts index 6fee0320b0..cf953c9698 100644 --- a/src/question_ranking.ts +++ b/src/question_ranking.ts @@ -217,7 +217,7 @@ export class QuestionRankingModel extends QuestionCheckboxModel { private getChoicesAnimation(isRankingChoices: boolean): IAnimationConsumer<[ItemValue]> { return { - isAnimationEnabled: () => settings.animationEnabled && this.animationAllowed, + isAnimationEnabled: () => this.animationAllowed, getLeaveOptions: (item: ItemValue) => { const choices = isRankingChoices ? this.rankingChoices : this.unRankingChoices; if(this.renderedSelectToRankAreasLayout == "vertical" && choices.length == 1 && choices.indexOf(item) >= 0) { diff --git a/src/react/components/matrix/row.tsx b/src/react/components/matrix/row.tsx index 66f66044a5..a912f70267 100644 --- a/src/react/components/matrix/row.tsx +++ b/src/react/components/matrix/row.tsx @@ -10,6 +10,7 @@ interface IMatrixRowProps { } export class MatrixRow extends SurveyElementBase { + private root: React.RefObject = React.createRef(); constructor(props: IMatrixRowProps) { super(props); } @@ -26,11 +27,37 @@ export class MatrixRow extends SurveyElementBase { this.parentMatrix.onPointerDown(event.nativeEvent, this.model.row); } + componentDidMount(): void { + super.componentDidMount(); + if(this.root.current) { + this.model.setRootElement(this.root.current); + } + } + + componentWillUnmount(): void { + super.componentWillUnmount(); + this.model.setRootElement(undefined); + } + + public shouldComponentUpdate(nextProps: any, nextState: any): boolean { + if (!super.shouldComponentUpdate(nextProps, nextState)) return false; + if (nextProps.model !== this.model) { + if(nextProps.element) { + nextProps.element.setRootElement(this.root.current); + } + if(this.model) { + this.model.setRootElement(undefined); + } + } + return true; + } + render() { const model = this.model; if(!model.visible) return null; return (
this.onPointerDownHandler(event)} diff --git a/src/react/reactquestion_matrixdropdownbase.tsx b/src/react/reactquestion_matrixdropdownbase.tsx index f66a86a8a0..14800f2455 100644 --- a/src/react/reactquestion_matrixdropdownbase.tsx +++ b/src/react/reactquestion_matrixdropdownbase.tsx @@ -121,7 +121,7 @@ export class SurveyQuestionMatrixDropdownBase extends SurveyQuestionElementBase renderRows(): JSX.Element { var cssClasses = this.question.cssClasses; var rows:Array = []; - var renderedRows = this.question.renderedTable.rows; + var renderedRows = this.question.renderedTable.renderedRows; for (var i = 0; i < renderedRows.length; i++) { rows.push( this.renderRow(renderedRows[i].id, renderedRows[i], cssClasses) diff --git a/src/react/reactquestion_paneldynamic.tsx b/src/react/reactquestion_paneldynamic.tsx index 9154c5e39e..978eb605bf 100644 --- a/src/react/reactquestion_paneldynamic.tsx +++ b/src/react/reactquestion_paneldynamic.tsx @@ -43,37 +43,17 @@ export class SurveyQuestionPanelDynamic extends SurveyQuestionElementBase { } protected renderElement(): JSX.Element { const panels:Array = []; - if (this.question.isRenderModeList) { - for (let i = 0; i < this.question.visiblePanels.length; i++) { - const panel = this.question.visiblePanels[i]; - panels.push( - - ); - } - } else { - if (this.question.currentPanel != null) { - const panel = this.question.currentPanel; - panels.push( - - ); - } - } + this.question.renderedPanels.forEach((panel, index) => { + panels.push(); + }); const btnAdd: JSX.Element | null = this.question.isRenderModeList && this.question["showLegacyNavigation"] ? this.renderAddRowButton() : null; @@ -84,14 +64,15 @@ export class SurveyQuestionPanelDynamic extends SurveyQuestionElementBase { ? this.renderNavigator() : null; - const style: any = {}; const navV2 = this.renderNavigatorV2(); const noEntriesPlaceholder = this.renderPlaceholder(); return (
{noEntriesPlaceholder} {navTop} - {panels} +
+ {panels} +
{navBottom} {btnAdd} {navV2} diff --git a/src/survey-element.ts b/src/survey-element.ts index 47143a643e..58f86d22e7 100644 --- a/src/survey-element.ts +++ b/src/survey-element.ts @@ -172,7 +172,7 @@ export class SurveyElement extends SurveyElementCore implements ISurvey const el = root.getElementById(elementId); return SurveyElement.ScrollElementToViewCore(el, false, scrollIfVisible); } - private static ScrollElementToViewCore(el: HTMLElement, checkLeft: boolean, scrollIfVisible?: boolean): boolean { + public static ScrollElementToViewCore(el: HTMLElement, checkLeft: boolean, scrollIfVisible?: boolean, scrollIntoViewOptions?: ScrollIntoViewOptions): boolean { if (!el || !el.scrollIntoView) return false; const elTop: number = scrollIfVisible ? -1 : el.getBoundingClientRect().top; let needScroll = elTop < 0; @@ -190,7 +190,7 @@ export class SurveyElement extends SurveyElementCore implements ISurvey } } if (needScroll) { - el.scrollIntoView(); + el.scrollIntoView(scrollIntoViewOptions); } return needScroll; } @@ -1091,7 +1091,7 @@ export class SurveyElement extends SurveyElementCore implements ISurvey } return undefined; }, - isAnimationEnabled: () => settings.animationEnabled && this.animationAllowed && !this.isDesignMode + isAnimationEnabled: () => this.animationAllowed && !this.isDesignMode }; } @@ -1115,7 +1115,7 @@ export class SurveyElement extends SurveyElementCore implements ISurvey private animationAllowedValue: boolean = true; public get animationAllowed(): boolean { - return !this.isLoadingFromJson && !this.isDisposed && !!this.survey && this.animationAllowedValue; + return settings.animationEnabled && !this.isLoadingFromJson && !this.isDisposed && !!this.survey && this.animationAllowedValue; } public set animationAllowed(val: boolean) { diff --git a/src/utils/animation.ts b/src/utils/animation.ts index 7af6d493d4..65e004cbff 100644 --- a/src/utils/animation.ts +++ b/src/utils/animation.ts @@ -19,6 +19,9 @@ export class AnimationUtils { if (value === "auto") return 0; return Number(value.slice(0, -1).replace(",", ".")) * 1000; } + private reflow(element: HTMLElement) { + return element.offsetHeight; + } private getAnimationsCount(element: HTMLElement) { let animationName = ""; if(getComputedStyle) { @@ -38,6 +41,15 @@ export class AnimationUtils { } private cancelQueue: Array<() => void> = []; + private addCancelCallback(callback: () => void) { + this.cancelQueue.push(callback); + } + private removeCancelCallback(callback: () => void) { + if(this.cancelQueue.indexOf(callback) >= 0) { + this.cancelQueue.splice(this.cancelQueue.indexOf(callback), 1); + } + } + protected onAnimationEnd(element: HTMLElement, callback: (isCancel?: boolean) => void, options: AnimationOptions): void { let cancelTimeout: any; let animationsCount = this.getAnimationsCount(element); @@ -45,7 +57,7 @@ export class AnimationUtils { options.onAfterRunAnimation && options.onAfterRunAnimation(element); callback(isCancel); clearTimeout(cancelTimeout); - this.cancelQueue.splice(this.cancelQueue.indexOf(onEndCallback), 1); + this.removeCancelCallback(onEndCallback); element.removeEventListener("animationend", onAnimationEndCallback); }; const onAnimationEndCallback = (event: AnimationEvent) => { @@ -55,7 +67,7 @@ export class AnimationUtils { }; if(animationsCount > 0) { element.addEventListener("animationend", onAnimationEndCallback); - this.cancelQueue.push(onEndCallback); + this.addCancelCallback(onEndCallback); cancelTimeout = setTimeout(() => { onEndCallback(false); }, this.getAnimationDuration(element) + 10); @@ -65,38 +77,48 @@ export class AnimationUtils { } protected beforeAnimationRun(element: HTMLElement, options: AnimationOptions | AnimationOptions): void { - if(element) { + if(element && options) { options.onBeforeRunAnimation && options.onBeforeRunAnimation(element); } } - protected runLeaveAnimation(element: HTMLElement, options: AnimationOptions, callback: () => void): void { + + protected runAnimation(element: HTMLElement, options: AnimationOptions, callback: (isCancel?: boolean) => void): void { if(element && options.cssClass) { + this.reflow(element); element.classList.add(options.cssClass); - const onAnimationEndCallback = (isCancel?: boolean) => { + this.onAnimationEnd(element, callback, options); + } else { + callback(true); + } + } + protected clearHtmlElement(element: HTMLElement, options: AnimationOptions): void { + if(element && options.cssClass) { + element.classList.remove(options.cssClass); + } + } + + protected onNextRender(callback: () => void, runEarly?: () => boolean, isCancel: boolean = false): void { + if(!isCancel && DomWindowHelper.isAvailable()) { + const cancelCallback = () => { callback(); - if(isCancel) { - element.classList.remove(options.cssClass); + cancelAnimationFrame(latestRAF); + }; + let latestRAF = DomWindowHelper.requestAnimationFrame(() => { + if(runEarly && runEarly()) { + callback(); + this.removeCancelCallback(cancelCallback); } else { - DomWindowHelper.requestAnimationFrame(() => { - DomWindowHelper.requestAnimationFrame(() => { - element.classList.remove(options.cssClass); - }); + latestRAF = DomWindowHelper.requestAnimationFrame(() => { + callback(); + this.removeCancelCallback(cancelCallback); }); } - }; - this.onAnimationEnd(element, onAnimationEndCallback, options); + }); + this.addCancelCallback(cancelCallback); } else { callback(); } } - protected runEnterAnimation(element: HTMLElement, options: AnimationOptions): void { - if(element && options.cssClass) { - element.classList.add(options.cssClass); - this.onAnimationEnd(element, () => { - element.classList.remove(options.cssClass); - }, options); - } - } public cancel(): void { const cancelQueue = [].concat(this.cancelQueue); @@ -106,63 +128,71 @@ export class AnimationUtils { } export class AnimationPropertyUtils extends AnimationUtils { - public onEnter(getElement: () => HTMLElement, options: AnimationOptions): void { - const callback = () => { - const element = getElement(); - this.beforeAnimationRun(element, options); - this.runEnterAnimation(element, options); - }; - DomWindowHelper.requestAnimationFrame(() => { - if(getElement()) { - callback(); - } else { - DomWindowHelper.requestAnimationFrame(callback); - } - }); + public onEnter(options: IAnimationConsumer): void { + this.onNextRender( + () => { + const htmlElement = options.getAnimatedElement(); + const enterOptions = options.getEnterOptions(); + this.beforeAnimationRun(htmlElement, enterOptions); + this.runAnimation(htmlElement, enterOptions, () => { + this.clearHtmlElement(htmlElement, enterOptions); + }); + }, + () => !!options.getAnimatedElement()); } - public onLeave(getElement: () => HTMLElement, callback: () => void, options: AnimationOptions): void { - const element = getElement(); - this.beforeAnimationRun(element, options); - this.runLeaveAnimation(element, options, callback); + public onLeave(options: IAnimationConsumer, callback: () => void): void { + const htmlElement = options.getAnimatedElement(); + const leaveOptions = options.getLeaveOptions(); + this.beforeAnimationRun(htmlElement, leaveOptions); + this.runAnimation(htmlElement, leaveOptions, (isCancel) => { + callback(); + this.onNextRender(() => { + this.clearHtmlElement(htmlElement, leaveOptions); + }, undefined, isCancel); + }); } } export class AnimationGroupUtils extends AnimationUtils { - public onEnter(getElement: (el: T) => HTMLElement, getOptions: (el: T) => AnimationOptions, elements: Array): void { - if(elements.length == 0) return; - DomWindowHelper.requestAnimationFrame(() => { - const callback = () => { - elements.forEach((el) => { - this.beforeAnimationRun(getElement(el), getOptions(el)); + public runGroupAnimation(options: IAnimationConsumer<[T]>, addedElements: Array, removedElements: Array, callback?: () => void): void { + this.onNextRender( + () => { + const addedHtmlElements = addedElements.map((el) => options.getAnimatedElement(el)); + const enterOptions = addedElements.map((el) => options.getEnterOptions(el)); + const removedHtmlElements = removedElements.map((el) => options.getAnimatedElement(el)); + const leaveOptions = removedElements.map((el) => options.getLeaveOptions(el)); + addedElements.forEach((_, i) => { + this.beforeAnimationRun(addedHtmlElements[i], enterOptions[i]); }); - elements.forEach((el) => { - this.runEnterAnimation(getElement(el), getOptions(el)); + removedElements.forEach((_, i) => { + this.beforeAnimationRun(removedHtmlElements[i], leaveOptions[i]); }); - }; - if(!getElement(elements[0])) { - DomWindowHelper.requestAnimationFrame(callback); - } else { - callback(); - } - }); - } - public onLeave(getElement: (el: T) => HTMLElement, callback: () => void, getOptions: (el: T) => AnimationOptions, elements: Array): void { - elements.forEach((el) => { - this.beforeAnimationRun(getElement(el), getOptions(el)); - }); - let counter = elements.length; - const onEndCallback = () => { - if (--counter <= 0) { - callback(); - } - }; - elements.forEach((el) => { - this.runLeaveAnimation(getElement(el), getOptions(el), onEndCallback); - }); + let counter = addedElements.length + removedElements.length; + const onAnimationEndCallback = (isCancel: boolean) => { + if(--counter <=0) { + callback && callback(); + this.onNextRender(() => { + addedElements.forEach((_, i) => { + this.clearHtmlElement(addedHtmlElements[i], enterOptions[i]); + }); + removedElements.forEach((_, i) => { + this.clearHtmlElement(removedHtmlElements[i], leaveOptions[i]); + }); + }, undefined, isCancel); + } + }; + addedElements.forEach((_, i) => { + this.runAnimation(addedHtmlElements[i], enterOptions[i], onAnimationEndCallback); + }); + removedElements.forEach((_, i) => { + this.runAnimation(removedHtmlElements[i], leaveOptions[i], onAnimationEndCallback); + }); + }, + () => addedElements.length == 0 || addedElements.some(el => !!options.getAnimatedElement(el))); } } -abstract class AnimationProperty = []> { - constructor(protected animationOptions: IAnimationConsumer, protected update: (val: T) => void, protected getCurrentValue: () => T) { +export abstract class AnimationProperty = []> { + constructor(protected animationOptions: IAnimationConsumer, protected update: (val: T, isTempUpdate?: boolean) => void, protected getCurrentValue: () => T) { } protected animation: AnimationUtils; protected abstract _sync(newValue: T): void; @@ -189,11 +219,11 @@ export class AnimationBoolean extends AnimationProperty { if(newValue !== this.getCurrentValue()) { if(newValue) { this.update(newValue); - this.animation.onEnter(() => this.animationOptions.getAnimatedElement(), this.animationOptions.getEnterOptions()); + this.animation.onEnter(this.animationOptions); } else { - this.animation.onLeave(() => this.animationOptions.getAnimatedElement(), () => { + this.animation.onLeave(this.animationOptions, () => { this.update(newValue); - }, this.animationOptions.getLeaveOptions()); + }); } } else { this.update(newValue); @@ -207,13 +237,29 @@ export class AnimationGroup extends AnimationProperty, [T]> { const oldValue = this.getCurrentValue(); const itemsToAdd = newValue.filter(el => oldValue.indexOf(el) < 0); const deletedItems = oldValue.filter(el => newValue.indexOf(el) < 0); - this.animation.onEnter((el) => this.animationOptions.getAnimatedElement(el), (el) => this.animationOptions.getEnterOptions(el), itemsToAdd); if (itemsToAdd.length == 0 && deletedItems?.length > 0) { - this.animation.onLeave((el) => this.animationOptions.getAnimatedElement(el), () => { - this.update(newValue); - }, (el) => this.animationOptions.getLeaveOptions(el), deletedItems); + this.animation.runGroupAnimation(this.animationOptions, [], deletedItems, () => this.update(newValue)); } else { this.update(newValue); + this.animation.runGroupAnimation(this.animationOptions, itemsToAdd, []); } } } +export class AnimationTab extends AnimationProperty, [T]> { + protected animation: AnimationGroupUtils = new AnimationGroupUtils(); + constructor(animationOptions: IAnimationConsumer<[T]>, update: (val: Array, isTempUpdate?: boolean) => void, getCurrentValue: () => Array, protected mergeValues?: (newValue: Array, oldValue: Array) => Array) { + super(animationOptions, update, getCurrentValue); + } + protected _sync(newValue: [T]): void { + const oldValue = [].concat(this.getCurrentValue()); + if(oldValue[0] !== newValue[0]) { + const tempValue = !!this.mergeValues ? this.mergeValues(newValue, oldValue) : [].concat(oldValue, newValue); + this.update(tempValue, true); + this.animation.runGroupAnimation(this.animationOptions, newValue, oldValue, () => { + this.update(newValue); + }); + } else { + this.update(newValue); + } + } +} \ No newline at end of file diff --git a/src/vue/paneldynamic.vue b/src/vue/paneldynamic.vue index 2d82893cd3..4c0fdb5de5 100644 --- a/src/vue/paneldynamic.vue +++ b/src/vue/paneldynamic.vue @@ -17,13 +17,15 @@ v-if="showLegacyNavigation && question.isProgressTopShowing" :question="question" /> - +
+ +
{ - get renderedPanels(): PanelModel[] { - if (this.question.isRenderModeList) return this.question.visiblePanels; - const panels = []; - if (this.question.currentPanel) { - panels.push(this.question.currentPanel); - } - return panels; - } get showLegacyNavigation() { return this.question["showLegacyNavigation"]; } diff --git a/tests/components/popuptests.ts b/tests/components/popuptests.ts index 73144c301d..b921d8fd48 100644 --- a/tests/components/popuptests.ts +++ b/tests/components/popuptests.ts @@ -1653,13 +1653,13 @@ class TestAnimation extends AnimationUtils { public logger: { log: string }; public passedEnterClass: string; public passedLeaveClass: string; - public onEnter(getElement: () => HTMLElement, options: AnimationOptions): void { + public onEnter(options: PopupBaseViewModel): void { this.logger.log += "->onEnter"; - this.passedEnterClass = options.cssClass; + this.passedEnterClass = options.getEnterOptions().cssClass; } - public onLeave(element: () => HTMLElement, callback: () => void, options: AnimationOptions): void { + public onLeave(options: PopupBaseViewModel, callback: () => void): void { this.logger.log += "->onLeave"; - this.passedLeaveClass = options.cssClass; + this.passedLeaveClass = options.getLeaveOptions().cssClass; callback(); } } diff --git a/tests/markup/snapshots/paneldynamic-list-legacy-navigation.snap.html b/tests/markup/snapshots/paneldynamic-list-legacy-navigation.snap.html index 03476add12..616b05341f 100644 --- a/tests/markup/snapshots/paneldynamic-list-legacy-navigation.snap.html +++ b/tests/markup/snapshots/paneldynamic-list-legacy-navigation.snap.html @@ -1,10 +1,14 @@
-
-
-
+
+
+
+
+
\ No newline at end of file diff --git a/tests/markup/snapshots/paneldynamic-list.snap.html b/tests/markup/snapshots/paneldynamic-list.snap.html index 83b99cb18c..be4f81762e 100644 --- a/tests/markup/snapshots/paneldynamic-list.snap.html +++ b/tests/markup/snapshots/paneldynamic-list.snap.html @@ -1,8 +1,10 @@
-
-
-
-
+
+
+
+
+
+
- - - - - - + + + + + + +