🐛 Make Mission Control a proper modal dialog#4339
Conversation
Convert Mission Control from a full-screen opaque overlay to a proper modal with backdrop, inset panel, rounded corners, and "Back to Dashboard" navigation. This resolves user confusion about whether Mission Control is a separate page or a layered UI element. Changes: - Add semi-transparent backdrop with blur so the underlying page is visible, reinforcing that this is an overlay - Inset the dialog panel 16px from viewport edges with rounded corners and a drop shadow to distinguish it from the page - Add "Back to Dashboard" link with arrow icon in the header - Make close (X) button larger with a destructive hover state and "Close (Esc)" tooltip - Lock body scroll while the modal is open - Add proper ARIA attributes (role="dialog", aria-modal, aria-label) - Clicking the backdrop also closes the modal - Add scale/translate entrance animation for modal feel Fixes #4271 Signed-off-by: Andy Anderson <andy@clubanderson.com> Signed-off-by: Andrew Anderson <andy@clubanderson.com>
|
👋 Hey @clubanderson — thanks for opening this PR!
This is an automated message. |
✅ Deploy Preview for kubestellarconsole ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
/lgtm |
|
@clubanderson: you cannot LGTM your own PR. DetailsIn response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: clubanderson The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
There was a problem hiding this comment.
Pull request overview
Converts Mission Control from a full-screen, page-like overlay into a visually distinct modal dialog to address first-time-user confusion (Fixes #4271).
Changes:
- Adds a semi-transparent, blurred backdrop and an inset, rounded modal panel with shadow.
- Introduces clearer close/navigation affordances (Back to Dashboard control + more prominent close button).
- Adds basic modal behaviors (backdrop click-to-close, Escape-to-close, and body scroll locking) plus entrance animation.
| if (!open) return null | ||
|
|
There was a problem hiding this comment.
AnimatePresence exit animations won’t run as written because the component returns null immediately when open becomes false (if (!open) return null). That unmounts the AnimatePresence tree before Framer Motion can play the exit transitions. Consider always rendering AnimatePresence and moving the open conditional inside it (or controlling mount/unmount from the parent) so the backdrop/panel exit props actually take effect.
| if (!open) return null |
| // Escape to close | ||
| const handleKeyDown = useCallback( | ||
| (e: KeyboardEvent) => { | ||
| if (e.key === 'Escape') onClose() | ||
| }, | ||
| [onClose] | ||
| ) | ||
|
|
||
| // Track the highest phase the user has reached so they can click back to any visited phase | ||
| const currentStepIndex = PHASE_STEPS.findIndex((s) => s.key === state.phase) | ||
| const highestReachedRef = useRef(currentStepIndex) | ||
| if (currentStepIndex > highestReachedRef.current) { | ||
| highestReachedRef.current = currentStepIndex | ||
| } | ||
| const highestReached = highestReachedRef.current | ||
|
|
||
| useEffect(() => { | ||
| if (!open) return | ||
| document.addEventListener('keydown', handleKeyDown) | ||
| return () => document.removeEventListener('keydown', handleKeyDown) | ||
| }, [open, handleKeyDown]) | ||
|
|
||
| // Lock body scroll while modal is open so users cannot scroll the page behind it | ||
| useEffect(() => { | ||
| if (!open) return | ||
| const prev = document.body.style.overflow | ||
| document.body.style.overflow = 'hidden' | ||
| return () => { | ||
| document.body.style.overflow = prev | ||
| } | ||
| }, [open]) |
There was a problem hiding this comment.
This modal re-implements ESC handling and body scroll locking manually. The codebase already has a standardized, tested hook (useModalNavigation in web/src/lib/modals/useModalNavigation.ts) that covers Escape, scroll lock, and default-prevent behavior; using it here would reduce duplicated logic and keep modal behavior consistent across the app.
| {/* ── Modal panel ───────────────────────────────────────── */} | ||
| <motion.div | ||
| role="dialog" | ||
| aria-modal="true" | ||
| aria-label="Mission Control" | ||
| className="fixed z-[200] flex flex-col bg-background rounded-xl border border-border shadow-2xl shadow-black/30 overflow-hidden" | ||
| style={{ | ||
| inset: `${MODAL_INSET_PX}px`, | ||
| }} | ||
| initial={{ opacity: 0, scale: 0.97, y: 12 }} | ||
| animate={{ opacity: 1, scale: 1, y: 0 }} | ||
| exit={{ opacity: 0, scale: 0.97, y: 12 }} | ||
| transition={{ duration: 0.2, ease: 'easeOut' }} | ||
| > |
There was a problem hiding this comment.
For a “proper modal dialog”, focus should be trapped within the dialog while it’s open; otherwise Tab/Shift+Tab can move focus to underlying page controls behind the backdrop. The repo already has useModalFocusTrap (and a combined useModal) in web/src/lib/modals/useModalNavigation.ts; consider wiring a ref to the modal panel and enabling focus trap here.
🔄 Auto-Applying Copilot Code ReviewCopilot code review found 1 code suggestion(s) and 2 general comment(s). @copilot Please apply all of the following code review suggestions:
Also address these general comments:
Push all fixes in a single commit. Run Auto-generated by copilot-review-apply workflow. |
|
Thank you for your contribution! Your PR has been merged. Check out what's new:
Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey |
All three review points have been addressed in commit
Build passes ( |
if (!open) return nullso AnimatePresence exit animations runuseModalNavigationuseModalFocusTrapwith a ref on the modal panelhighestReachedRefpattern)