fix: resolve modal stacking, animation, and close cleanup bugs

- Set backdrop z-index:0 and content z-index:1 to fix backdrop covering
  modal content within the stacking context
- Force reflow between removing hidden and adding open class so CSS
  opacity transition fires correctly
- Add transitionend listener + setTimeout fallback to restore hidden
  class after close animation completes
- Override flex:1 on modal footer buttons to prevent full-width stretch
- Add min-width to modal size variants for consistent sizing
- Add 23 tests covering open/close lifecycle, keyboard, and destroy

Amit Haridas
This commit is contained in:
2026-03-25 22:20:34 +05:30
parent 5911a7501b
commit adc8dabda1
3 changed files with 314 additions and 3 deletions
+24 -2
View File
@@ -124,8 +124,12 @@ class ModalManager {
// Track open modals
ModalManager.#openModals.push(this);
// Show modal (remove hidden, add open)
// Show modal: remove hidden first, force a reflow so the browser
// records opacity:0 as the start state, then add 'open' to trigger
// the CSS transition. Without the reflow, both class changes are
// batched into one style recalculation and the transition is skipped.
this.#modal.classList.remove('hidden');
void this.#modal.offsetHeight; // Force reflow — do not remove
this.#modal.classList.add('open');
this.#isOpen = true;
@@ -165,10 +169,28 @@ class ModalManager {
ModalManager.#openModals.splice(index, 1);
}
// Hide modal
// Start hide transition
this.#modal.classList.remove('open');
this.#isOpen = false;
// Re-add 'hidden' after the CSS transition completes so the modal
// is fully removed from rendering (display:none), not just invisible.
// We use both transitionend and a setTimeout fallback because
// transitionend never fires when prefers-reduced-motion disables transitions.
let hidden = false;
const addHidden = () => {
if (hidden || this.#isOpen) return;
hidden = true;
this.#modal.removeEventListener('transitionend', onTransitionEnd);
this.#modal.classList.add('hidden');
};
const onTransitionEnd = (e) => {
if (e.target !== this.#modal) return;
addHidden();
};
this.#modal.addEventListener('transitionend', onTransitionEnd);
setTimeout(addHidden, 250); // fallback: slightly longer than 200ms transition
// Restore body scroll if no modals open
if (ModalManager.#openModals.length === 0) {
document.body.style.overflow = '';