Skip to content

Add AnimatePresence exit animations to NormReferenceModal portal in RequirementForm #79

@johlju

Description

@johlju

Summary

Add exit animations to the NormReferenceModal rendered via createPortal in components/RequirementForm.tsx. Currently the modal has enter animations (initial/animate) but no exit animation because AnimatePresence is not used.

Problem

In RequirementForm.tsx (lines 238–306), the modal is conditionally rendered with:

{showCreateNormRef &&
  createPortal(
    <NormReferenceModal ... />,
    document.body,
  )}

The NormReferenceModal component (line 468) uses motion.div with initial={{ opacity: 0, scale: 0.95 }} and animate={{ opacity: 1, scale: 1 }}, but since there is no wrapping AnimatePresence, the exit animation never plays — the modal disappears instantly when showCreateNormRef becomes false.

Why this needs care

createPortal returns a ReactPortal, not a motion component. Wrapping the conditional directly in AnimatePresence will not work because Framer Motion needs direct motion.* children to detect unmounts and play exit animations.

AI Implementation Plan

Approach: Always-mounted portal with AnimatePresence inside

Instead of conditionally rendering the portal, always render it and move the conditional inside:

Step 1 — Restructure the portal in RequirementForm.tsx

Replace (lines 238–306):

{showCreateNormRef &&
  createPortal(
    <NormReferenceModal ... />,
    document.body,
  )}

With:

{createPortal(
  <AnimatePresence>
    {showCreateNormRef && (
      <NormReferenceModal ... />
    )}
  </AnimatePresence>,
  document.body,
)}

Import AnimatePresence from framer-motion (already imported on line 3).

Step 2 — Add exit props to NormReferenceModal

In the NormReferenceModal component, update the root wrapper and motion.div:

  1. Change the root <div> (line 459) to <motion.div> with enter/exit animation for the backdrop:
<motion.div
  className="fixed inset-0 z-50 flex items-center justify-center p-4"
  ref={overlayRef}
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  exit={{ opacity: 0 }}
  transition={{ duration: 0.15 }}
>
  1. Add exit prop to the existing motion.div dialog (line 468):
exit={{ opacity: 0, scale: 0.95 }}

Step 3 — Verify focus management still works

The existing useEffect hooks for focus trapping (lines 404–448) should continue working since the modal component still mounts/unmounts inside AnimatePresence. Verify:

  • Focus moves to close button on open
  • Focus returns to previously focused element on close
  • Tab trapping works during the exit animation window

Step 4 — Update tests

Check tests/unit/ for any RequirementForm tests that assert on portal rendering or modal visibility. Update assertions to account for the always-mounted portal wrapper.

Step 5 — Verify

npx vitest run tests/unit/ --no-color
npm run check

Reference pattern

See ConfirmModal.tsx for the established AnimatePresence + motion.div enter/exit pattern used elsewhere in the codebase.

Files to modify

  • components/RequirementForm.tsx — restructure portal, add exit props to modal

Risk

  • Low: the change is isolated to the modal rendering in one component
  • Focus management must be tested since AnimatePresence delays unmount

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions