Skip to content

Commit 4ffc65e

Browse files
committed
Improve dialog swapping
1 parent 6ddee65 commit 4ffc65e

2 files changed

Lines changed: 38 additions & 0 deletions

File tree

js/src/dialog.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const EVENT_CANCEL = `cancel${EVENT_KEY}`
2727
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
2828

2929
const CLASS_NAME_NONMODAL = 'dialog-nonmodal'
30+
const CLASS_NAME_INSTANT = 'dialog-instant'
31+
const CLASS_NAME_SWAP_IN = 'dialog-swap-in'
3032

3133
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="dialog"]'
3234

@@ -130,11 +132,37 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
130132
const shouldSwap = currentDialog && currentDialog !== target
131133

132134
if (shouldSwap) {
135+
// Swap strategy (seamless backdrop, no flash):
136+
// 1. Mark the incoming dialog with .dialog-swap-in so its ::backdrop
137+
// skips the @starting-style fade-in and appears fully opaque on
138+
// its very first frame in the top layer.
139+
// 2. Open the incoming dialog (showModal).
140+
// 3. Close the outgoing dialog synchronously — no exit transition, no
141+
// .hiding — so its ::backdrop is removed in the same frame the
142+
// incoming dialog's backdrop appears. Since both backdrops render
143+
// the same color, the user sees one continuous backdrop. Two
144+
// simultaneously-visible backdrops would composite to ~75% darker,
145+
// and a fading-out + fading-in pair would dip to ~75% opacity —
146+
// either would look like a flash.
147+
// 4. Clean up the .dialog-swap-in flag once the incoming dialog
148+
// finishes its entry transition.
133149
const newDialog = Dialog.getOrCreateInstance(target, config)
150+
target.classList.add(CLASS_NAME_SWAP_IN)
134151
newDialog.show(this)
152+
EventHandler.one(target, `shown${EVENT_KEY}`, () => {
153+
target.classList.remove(CLASS_NAME_SWAP_IN)
154+
})
135155

136156
const currentInstance = Dialog.getInstance(currentDialog)
137157
if (currentInstance) {
158+
// Force synchronous close: .dialog-instant makes _isAnimated() false,
159+
// which makes _shouldDeferClose() false, so hide() calls close()
160+
// immediately (no deferred .hiding path). The class is removed after
161+
// the (now-synchronous) hidden event fires.
162+
currentDialog.classList.add(CLASS_NAME_INSTANT)
163+
EventHandler.one(currentDialog, EVENT_HIDDEN, () => {
164+
currentDialog.classList.remove(CLASS_NAME_INSTANT)
165+
})
138166
currentInstance.hide()
139167
}
140168

scss/_dialog.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,16 @@ $dialog-sizes: defaults(
240240
background-color: transparent;
241241
backdrop-filter: blur(0);
242242
}
243+
244+
// Swap entry: when this dialog is opened as the target of a swap, the
245+
// outgoing dialog's ::backdrop is being removed synchronously in the same
246+
// JS tick. To avoid any flicker (either a dip from a fade-in over nothing,
247+
// or double-darkening from two stacked backdrops), start this backdrop
248+
// already-opaque so it takes over from the outgoing one seamlessly.
249+
.dialog.dialog-swap-in:not(.dialog-instant)::backdrop {
250+
background-color: var(--dialog-backdrop-bg);
251+
backdrop-filter: blur(var(--dialog-backdrop-blur));
252+
}
243253
}
244254

245255
// Dialog sizes

0 commit comments

Comments
 (0)