Skip to content

fix(dialog-box): dialog pop-up mask layer animation is not working (#… #3637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
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
61 changes: 59 additions & 2 deletions packages/theme/src/dialog-box/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,6 @@
top: 0;
width: 100%;
height: 100%;
background: var(--tv-DialogBox-mask-bg-color);
}

.v-modal-enter {
Expand All @@ -230,10 +229,16 @@

.dialog-slideRight-enter-active {
animation: slideRight 0.5s ease-in forwards;
&.tiny-dialog-box__wrapper {
animation: modal-fade-in 0.5s;
}
}
Comment on lines +232 to 235
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Ensure mask fade has correct fill-mode to avoid end-frame flicker

Add fill-mode to the wrapper’s modal fade animations. Without it, the mask can momentarily snap back to full opacity at the end of leave or have a first-frame pop on enter.

 .dialog-slideRight-enter-active {
   animation: slideRight 0.5s ease-in forwards;
   &.tiny-dialog-box__wrapper {
-    animation: modal-fade-in 0.5s;
+    animation: modal-fade-in 0.5s both;
   }
 }

 .dialog-slideRight-leave-active {
   animation: slideRightout 0.5s ease-in forwards;
   &.tiny-dialog-box__wrapper {
-    animation: modal-fade-out 0.5s;
+    animation: modal-fade-out 0.5s forwards;
   }
 }

Also applies to: 239-242

🤖 Prompt for AI Agents
In packages/theme/src/dialog-box/index.less around lines 232-235 (and also apply
same change at 239-242), the modal-fade-in/out animations lack an
animation-fill-mode which causes end-frame flicker; add an appropriate
animation-fill-mode (preferably "both" to preserve start and end states) to the
wrapper’s animation declarations so the mask retains its intended opacity at
both enter and leave transitions.


.dialog-slideRight-leave-active {
animation: slideRightout 0.5s ease-in forwards;
&.tiny-dialog-box__wrapper {
animation: modal-fade-out 0.5s;
}
}

@keyframes slideRight {
Expand Down Expand Up @@ -270,15 +275,43 @@
}
}

.tiny-dialog-box__wrapper.has-modal {
background: var(--tv-DialogBox-mask-bg-color);
}

.dialog-fade-enter-active {
animation: dialog-fade-in 0.3s;
&.tiny-dialog-box__wrapper {
animation: modal-fade-in 0.3s;
}
}
Comment on lines +284 to 287
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Unify fade enter/leave fill-mode for consistency

Apply the same fill-mode pattern to dialog-fade enter/leave to prevent visual popping at the start/end of the animation.

 .dialog-fade-enter-active {
   animation: dialog-fade-in 0.3s;
   &.tiny-dialog-box__wrapper {
-    animation: modal-fade-in 0.3s;
+    animation: modal-fade-in 0.3s both;
   }
 }

 .dialog-fade-leave-active {
   .@{dialog-box-prefix-cls} {
     animation: dialog-fade-out 3s;
   }
   animation: dialog-fade-out 0.3s;
   &.tiny-dialog-box__wrapper {
-    animation: modal-fade-out 0.3s;
+    animation: modal-fade-out 0.3s forwards;
   }
 }

Also applies to: 294-297

🤖 Prompt for AI Agents
In packages/theme/src/dialog-box/index.less around lines 284-287 (and similarly
lines 294-297), the fade animations lack a fill-mode which causes visual popping
at animation start/end; update the relevant selectors (the
.tiny-dialog-box__wrapper and the dialog-fade enter/leave selectors referenced
at 294-297) to include animation-fill-mode: both (or add animation: ... 0.3s
both) so the animated element retains start/end styles during the animation to
prevent popping.


.dialog-fade-leave-active {
.@{dialog-box-prefix-cls} {
animation: dialog-fade-out 3s;
}
animation: dialog-fade-out 0.3s;
&.tiny-dialog-box__wrapper {
animation: modal-fade-out 0.3s;
}
}
Comment on lines 289 to +297
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

3s fade-out on inner dialog likely unintended; align to 0.3s

A 3s fade on the inner panel during leave is disproportionately long vs. the 0.3s wrapper fade and other transitions. This looks like a typo.

 .dialog-fade-leave-active {
   .@{dialog-box-prefix-cls} {
-    animation: dialog-fade-out 3s;
+    animation: dialog-fade-out 0.3s;
   }
   animation: dialog-fade-out 0.3s;
   &.tiny-dialog-box__wrapper {
     animation: modal-fade-out 0.3s forwards;
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.dialog-fade-leave-active {
.@{dialog-box-prefix-cls} {
animation: dialog-fade-out 3s;
}
animation: dialog-fade-out 0.3s;
&.tiny-dialog-box__wrapper {
animation: modal-fade-out 0.3s;
}
}
.dialog-fade-leave-active {
.@{dialog-box-prefix-cls} {
animation: dialog-fade-out 0.3s;
}
animation: dialog-fade-out 0.3s;
&.tiny-dialog-box__wrapper {
animation: modal-fade-out 0.3s forwards;
}
}
🤖 Prompt for AI Agents
In packages/theme/src/dialog-box/index.less around lines 289 to 297, the inner
dialog rule sets animation: dialog-fade-out 3s which is likely a typo and
inconsistent with the wrapper's 0.3s duration; change the inner
.@{dialog-box-prefix-cls} animation duration from 3s to 0.3s so both inner panel
and wrapper use the same 0.3s fade-out timing and maintain consistent
transitions.


@keyframes modal-fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}

@keyframes modal-fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}

@keyframes dialog-fade-in {
Expand Down Expand Up @@ -306,7 +339,17 @@
}

.enlarge-enter-active {
animation: enlarge-in 350ms linear;
animation: enlarge-in 0.35s linear;
&.tiny-dialog-box__wrapper {
animation: modal-fade-in 0.35s;
}
}
Comment on lines 341 to +346
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Apply fill-mode to enlarge enter/leave wrapper fades

Keep consistency with other transitions and avoid end-of-animation flashes.

 .enlarge-enter-active {
   animation: enlarge-in 0.35s linear;
   &.tiny-dialog-box__wrapper {
-    animation: modal-fade-in 0.35s;
+    animation: modal-fade-in 0.35s both;
   }
 }

 .enlarge-leave-active {
   animation: enlarge-out 0.35s linear;
   &.tiny-dialog-box__wrapper {
-    animation: modal-fade-out 0.35s;
+    animation: modal-fade-out 0.35s forwards;
   }
 }

Also applies to: 348-353

🤖 Prompt for AI Agents
In packages/theme/src/dialog-box/index.less around lines 341-346 (and also for
the corresponding leave rules at 348-353), the enter/leave active animation
rules lack an animation-fill-mode which causes end-of-animation flashes; add
animation-fill-mode: both to the .enlarge-enter-active and its nested
.tiny-dialog-box__wrapper rule, and likewise add animation-fill-mode: both to
the .enlarge-leave-active and its nested wrapper rule so the animated state is
preserved before and after the animation.


.enlarge-leave-active {
animation: enlarge-out 0.35s linear;
&.tiny-dialog-box__wrapper {
animation: modal-fade-out 0.35s;
}
}

@keyframes enlarge-in {
Expand All @@ -323,6 +366,20 @@
}
}

@keyframes enlarge-out {
0% {
transform: scale(1);
}

50% {
transform: scale(0.5);
}

100% {
transform: scale(0);
}
}

@media (max-width: 480px) {
.@{dialog-box-prefix-cls} {
width: 100% !important;
Expand Down
2 changes: 1 addition & 1 deletion packages/vue/src/dialog-box/src/pc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
>
<div
v-show="visible"
:class="['tiny-dialog-box__wrapper', dialogClass]"
:class="['tiny-dialog-box__wrapper', dialogClass, modal ? 'has-modal' : '']"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Double-check modalAppendToBody interaction and pointer event flow

With the mask now on the wrapper, ensure:

  • When modalAppendToBody is true, the wrapper actually overlays the viewport (not trapped by transformed ancestors).
  • .v-modal (transparent) doesn’t sit above the wrapper and eat clicks.
  • Mobile viewport/scroll-lock behavior remains correct.

If issues surface, consider gating has-modal with actual append-to-body state or ensuring wrapper is teleported to body when masking.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate appendToBody/modalAppendToBody handling for dialog-box"
rg -n --no-heading -A 6 -B 6 -S 'appendToBody|modalAppendToBody' packages/vue/src/dialog-box

echo "Check any teleport/portal usage in dialog-box code"
rg -n --no-heading -A 2 -B 2 -S 'Teleport|teleport|appendChild' packages/vue/src/dialog-box packages/vue-renderless

Length of output: 3989


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "==== pc.vue (showing with line numbers) ===="
nl -ba packages/vue/src/dialog-box/src/pc.vue | sed -n '1,360p' || true

echo
echo "==== mobile-first.vue (showing with line numbers) ===="
nl -ba packages/vue/src/dialog-box/src/mobile-first.vue | sed -n '1,360p' || true

echo
echo "==== Search for Teleport / teleport in packages/vue ===="
rg -n --no-heading -S 'Teleport|teleport' packages/vue || true

echo
echo "==== Search for appendToBody/modalAppendToBody usage across repo ===="
rg -n --no-heading -S 'appendToBody|modalAppendToBody' || true

echo
echo "==== Search for class 'has-modal' usage across repo ===="
rg -n --no-heading -S "has-modal" || true

Length of output: 8541


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "==== packages/vue/src/dialog-box/src/pc.vue ===="
sed -n '1,400p' packages/vue/src/dialog-box/src/pc.vue || true

echo
echo "==== packages/vue/src/dialog-box/src/mobile-first.vue ===="
sed -n '1,400p' packages/vue/src/dialog-box/src/mobile-first.vue || true

echo
echo "==== packages/vue/src/dialog-box/src/index.ts (props) ===="
sed -n '1,240p' packages/vue/src/dialog-box/src/index.ts || true

echo
echo "==== packages/renderless/src/dialog-box/index.ts ===="
sed -n '1,420p' packages/renderless/src/dialog-box/index.ts || true

echo
echo "==== packages/vue-hooks/src/vue-popup.ts ===="
sed -n '1,420p' packages/vue-hooks/src/vue-popup.ts || true

echo
echo "==== packages/theme/src/dialog-box/index.less (around has-modal) ===="
sed -n '250,320p' packages/theme/src/dialog-box/index.less || true

Length of output: 30798


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Search for tiny-dialog-box__wrapper usages in theme and styles"
rg -n --no-heading -S "tiny-dialog-box__wrapper" || true

echo
echo "Show full dialog-box theme file"
sed -n '1,400p' packages/theme/src/dialog-box/index.less || true

echo
echo "Search for PopupManager implementation in repo"
rg -n --no-heading -S "class PopupManager|export const PopupManager|function PopupManager|PopupManager =" || true

echo
echo "Search for openModal implementation"
rg -n --no-heading -S "openModal(" || true

echo
echo "Search for v-modal or modal class used by PopupManager (common names)"
rg -n --no-heading -S "v-modal|modal-mask|pop-modal|popLockClass|pop-modal" || true

Length of output: 12889


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "==== packages/utils/src/popup-manager/index.ts ===="
sed -n '1,400p' packages/utils/src/popup-manager/index.ts || true

Length of output: 6949


Action required — modalAppendToBody vs wrapper mask can be out-of-sync

Short summary: I verified the code paths. modalAppendToBody controls where PopupManager inserts the global .v-modal, while appendToBody controls whether the dialog wrapper (tiny-dialog-box__wrapper) is moved to body. The wrapper now carries the visual mask (.has-modal). That combination (modalAppendToBody === true && appendToBody === false — both are defaults) can leave the wrapper trapped by transformed ancestors (so it won't cover the viewport) or otherwise cause layering/pointer issues.

What I checked (evidence):

  • packages/vue/src/dialog-box/src/pc.vue — wrapper class binding (current line):
    :class="['tiny-dialog-box__wrapper', dialogClass, modal ? 'has-modal' : '']"
  • packages/renderless/src/dialog-box/index.ts — appends parent.$el to document.body only when props.appendToBody is true.
  • packages/vue-hooks/src/vue-popup.ts — passes props.modalAppendToBody to PopupManager.openModal.
  • packages/utils/src/popup-manager/index.ts — openModal appends the modal DOM to document.body or dom.parentNode and manages z-index.
  • packages/theme/src/dialog-box/index.less — .tiny-dialog-box__wrapper.has-modal provides the mask; .v-modal also exists.

Recommended fixes (pick one):

  1. Minimal / safest: only apply has-modal when the wrapper actually overlays the viewport:
  • packages/vue/src/dialog-box/src/pc.vue — change the class binding to gate has-modal:
- :class="['tiny-dialog-box__wrapper', dialogClass, modal ? 'has-modal' : '']"
+ :class="['tiny-dialog-box__wrapper', dialogClass, (modal && (appendToBody || !modalAppendToBody)) ? 'has-modal' : '']"

Rationale: if modalAppendToBody === true but the wrapper is not appended to body, don't apply the wrapper mask (avoid trapped/incomplete mask). That leaves PopupManager/.v-modal to provide the mask.

  1. More robust (recommended if you want the visual mask always placed on the wrapper): ensure the wrapper is teleported to body whenever modal masking is used:
  • packages/renderless/src/dialog-box/index.ts — in watchVisible/mounted/unMounted, treat modalAppendToBody the same as appendToBody for moving the wrapper:
- if (props.appendToBody) {
+ if (props.appendToBody || (props.modal && props.modalAppendToBody)) {
     document.body.appendChild(el)
 }

(and mirror the same predicate when removing the element in unMounted).

Rationale: keeps visual mask (has-modal) on the wrapper and avoids transformed-ancestor containment.

Extra checks to run after applying either fix:

  • Verify stacking/z-index order between .v-modal and the wrapper still yields the correct visual and pointer behavior.
  • Test cases where the component is inside a transformed ancestor.
  • Mobile scroll-lock and touch behavior (PopupManager adds touch handlers and popLockClass) — ensure no regressions.

Files to review:

  • packages/vue/src/dialog-box/src/pc.vue (class binding)
  • packages/renderless/src/dialog-box/index.ts (append/remove logic)
  • packages/vue-hooks/src/vue-popup.ts (modalAppendToBody usage)
  • packages/utils/src/popup-manager/index.ts (openModal/z-index behavior)
  • packages/theme/src/dialog-box/index.less (mask styles)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
:class="['tiny-dialog-box__wrapper', dialogClass, modal ? 'has-modal' : '']"
:class="['tiny-dialog-box__wrapper', dialogClass, (modal && (appendToBody || !modalAppendToBody)) ? 'has-modal' : '']"
🤖 Prompt for AI Agents
packages/vue/src/dialog-box/src/pc.vue lines 21-21: the wrapper class binding
applies .has-modal whenever modal is true, but modalAppendToBody can cause the
global .v-modal to be appended to body while the wrapper remains inside its
original ancestor, causing mask/coverage and pointer issues; fix by gating the
has-modal class so it is only added when the wrapper is actually appended to
body (i.e., modal && (appendToBody || modalAppendToBodyEquivalentFlag)), or
alternatively update packages/renderless/src/dialog-box/index.ts to treat
modalAppendToBody the same as appendToBody and move the wrapper to document.body
whenever modal masking is used (and mirror removal in unMounted) — pick one:
either change the class condition in pc.vue to only add has-modal when the
wrapper is moved to body, or change the renderless logic to ensure the wrapper
is teleported to body when modal masking is enabled.

@click.self="handleWrapperClick"
@mouseup="useMouseEventUp"
@mousedown="useMouseEventDown"
Expand Down