zurich_kotak/src/components/NewLeadDialog.jsx
Yashas 119bea1e76 fix(console): centre modals via a portal, not inside a transformed ancestor
The New Lead form opened off-centre and its backdrop covered only part of the
screen. Cause: the dialog uses position:fixed, but .shell__main animates
transform on entrance, and an element that has animated transform keeps acting
as the containing block in Chrome even after the animation finishes. So "fixed"
resolved against the 1460px max-width, margin-auto content area — offset by the
sidebar — instead of the viewport.

Every scrim dialog shared this latent bug. Each now renders through
createPortal(…, document.body), so the fixed overlay is always relative to the
viewport regardless of any transformed ancestor. New Lead, the conversation and
call threads, the full lead file, the agent card, and the reasoning dialog all
now centre and dim the whole screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 01:28:37 +05:30

113 lines
4.3 KiB
JavaScript

import { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useNavigate } from 'react-router-dom'
import ActivityForm from './ActivityForm.jsx'
import { ENTRY } from '../api/config.js'
import { entryDoorsFor, rolesOf } from '../api/permissions.js'
import { useZino } from '../api/provider.jsx'
import './NewLeadDialog.css'
/**
* Filing a lead, over the page rather than on one of its own.
*
* This was a route. Pressing New lead left whatever you were reading — a queue
* you were working through, a lead you were half way down — filed the form,
* and landed you on the new lead's file, three navigations from where you
* started. Filing a lead is not a destination; it is four fields and a Submit,
* and it belongs over the work rather than instead of it. Cancel now puts you
* back exactly where you were, because you never left.
*
* The door chooser stays: the entry activities have different permissions and
* the bank one lands the instance further along. With one door — which is
* today — it is skipped rather than shown as a list of one.
*
* Modal furniture is the same as the conversation and reasoning dialogs:
* scrim click, Escape, focus trapped in and restored out, page scroll frozen.
* A fourth hand-rolled variant of that would be a fourth chance to get the
* keyboard wrong.
*/
export default function NewLeadDialog({ onClose }) {
const { user } = useZino()
const doors = entryDoorsFor(rolesOf(user), ENTRY)
const [entry, setEntry] = useState(doors.length === 1 ? doors[0] : null)
const navigate = useNavigate()
const ref = useRef(null)
const restore = useRef(null)
useEffect(() => {
restore.current = document.activeElement
ref.current?.focus()
const onKey = (e) => { if (e.key === 'Escape') onClose() }
document.addEventListener('keydown', onKey)
const prev = document.body.style.overflow
document.body.style.overflow = 'hidden'
return () => {
document.removeEventListener('keydown', onKey)
document.body.style.overflow = prev
if (restore.current instanceof HTMLElement) restore.current.focus()
}
}, [onClose])
return createPortal(
<div className="prose__scrim" onClick={onClose} role="presentation">
<div
className="prose__dlg nld"
role="dialog"
aria-modal="true"
aria-label="New lead"
tabIndex={-1}
ref={ref}
onClick={(e) => e.stopPropagation()}
>
<div className="prose__head">
<div>
<h3>{entry ? entry.label : 'New lead'}</h3>
<p className="nld__sub">
{entry
? entry.note
: 'Capture the customer, the vehicle and the renewal date. Everything after this runs automatically.'}
</p>
</div>
<button type="button" onClick={onClose} aria-label="Close">
<svg viewBox="0 0 14 14" aria-hidden="true">
<path d="M3.5 3.5l7 7M10.5 3.5l-7 7" fill="none" stroke="currentColor"
strokeWidth="1.6" strokeLinecap="round" />
</svg>
</button>
</div>
<div className="nld__body">
{!doors.length ? (
<p className="empty">Your role does not create leads.</p>
) : !entry ? (
<div className="doors">
{doors.map((e) => (
<button key={e.uid} type="button" className="door" onClick={() => setEntry(e)}>
<strong>{e.label}</strong>
<span className="chip">{e.channel}</span>
<p>{e.note}</p>
</button>
))}
</div>
) : (
<ActivityForm
activityUid={entry.uid}
onCancel={onClose}
onDone={(res) => {
const id = res?.instance_id || res?.data?.instance_id
// Carry the workflow's own message across the navigation —
// "Lead received — Intake is qualifying it" is the answer to
// "did that work?", and it is only said once.
onClose()
if (id) navigate(`/lead/${id}`, { state: { message: res?.message ?? null } })
}}
/>
)}
</div>
</div>
</div>
,
document.body,
)
}