22
33import Image from "next/image" ;
44import { useEffect , useId , useRef , useState } from "react" ;
5+ import { createPortal } from "react-dom" ;
56import type { PeopleGroups , Person } from "@/lib/people-schema" ;
67
78function TeamMemberCard ( {
@@ -153,21 +154,73 @@ function AlumniDetailModal({
153154} ) {
154155 const titleId = useId ( ) ;
155156 const closeButtonRef = useRef < HTMLButtonElement > ( null ) ;
157+ const dialogRef = useRef < HTMLDivElement > ( null ) ;
158+ const backdropRef = useRef < HTMLDivElement > ( null ) ;
159+ const [ mounted , setMounted ] = useState ( false ) ;
156160
157161 useEffect ( ( ) => {
158- closeButtonRef . current ?. focus ( ) ;
162+ setMounted ( true ) ;
163+ } , [ ] ) ;
164+
165+ // Prevent background scrolling while modal is open without removing body scrollbar (avoids sideways layout shift)
166+ useEffect ( ( ) => {
167+ const backdrop = backdropRef . current ;
168+ if ( ! backdrop ) return ;
169+
170+ const handleWheel = ( e : WheelEvent ) => {
171+ if ( ! dialogRef . current ?. contains ( e . target as Node ) ) {
172+ e . preventDefault ( ) ;
173+ }
174+ } ;
175+
176+ const handleTouchMove = ( e : TouchEvent ) => {
177+ if ( ! dialogRef . current ?. contains ( e . target as Node ) ) {
178+ e . preventDefault ( ) ;
179+ }
180+ } ;
181+
182+ backdrop . addEventListener ( "wheel" , handleWheel , { passive : false } ) ;
183+ backdrop . addEventListener ( "touchmove" , handleTouchMove , { passive : false } ) ;
184+
185+ return ( ) => {
186+ backdrop . removeEventListener ( "wheel" , handleWheel ) ;
187+ backdrop . removeEventListener ( "touchmove" , handleTouchMove ) ;
188+ } ;
189+ } , [ ] ) ;
190+
191+ // Move focus into the modal on open, restore previous focus on close, and trap Tab navigation
192+ useEffect ( ( ) => {
193+ const trigger = document . activeElement as HTMLElement | null ;
194+ closeButtonRef . current ?. focus ( { preventScroll : true } ) ;
159195
160196 const handleKeyDown = ( event : KeyboardEvent ) => {
161197 if ( event . key === "Escape" ) {
162198 event . preventDefault ( ) ;
163199 onClose ( ) ;
200+ } else if ( event . key === "Tab" ) {
201+ const dialog = dialogRef . current ;
202+ if ( ! dialog ) return ;
203+ const focusables = dialog . querySelectorAll < HTMLElement > (
204+ 'button:not([disabled]), a[href]:not([disabled]), [tabindex]:not([tabindex="-1"])' ,
205+ ) ;
206+ if ( focusables . length === 0 ) return ;
207+ const first = focusables [ 0 ] ;
208+ const last = focusables [ focusables . length - 1 ] ;
209+ if ( event . shiftKey && document . activeElement === first ) {
210+ event . preventDefault ( ) ;
211+ last . focus ( ) ;
212+ } else if ( ! event . shiftKey && document . activeElement === last ) {
213+ event . preventDefault ( ) ;
214+ first . focus ( ) ;
215+ }
164216 }
165217 } ;
166218
167219 window . addEventListener ( "keydown" , handleKeyDown ) ;
168220
169221 return ( ) => {
170222 window . removeEventListener ( "keydown" , handleKeyDown ) ;
223+ trigger ?. focus ( { preventScroll : true } ) ;
171224 } ;
172225 } , [ onClose ] ) ;
173226
@@ -178,38 +231,42 @@ function AlumniDetailModal({
178231 ? [ member . title ]
179232 : [ ] ;
180233
181- return (
234+ if ( ! mounted ) return null ;
235+
236+ return createPortal (
182237 < div
183- className = "fixed inset-0 z-60 flex items-center justify-center p-4 bg-black/50 backdrop-blur-xs motion-safe:animate-fade-in"
238+ ref = { backdropRef }
239+ className = "fixed inset-0 z-[100] flex items-center justify-center p-4 sm:p-6 bg-black/60 backdrop-blur-xs motion-safe:animate-fade-in touch-none"
184240 onClick = { ( event ) => {
185241 if ( event . target === event . currentTarget ) onClose ( ) ;
186242 } }
187243 role = "presentation"
188244 >
189245 < div
246+ ref = { dialogRef }
190247 role = "dialog"
191248 aria-modal = "true"
192249 aria-labelledby = { titleId }
193- className = "relative w-full max-w-md rounded-2xl bg-card border border-border shadow-xl p-5 sm:p-6 text-left motion-safe:animate-slide-up-fade"
250+ className = "relative w-[92vw] sm:w-[85vw] md: max-w-xl max-h-[88vh] overflow-y-auto overscroll-contain rounded-2xl bg-card border border-border shadow-2xl p-6 sm:p-7 text-left motion-safe:animate-slide-up-fade touch-auto "
194251 >
195252 < button
196253 ref = { closeButtonRef }
197254 type = "button"
198255 onClick = { onClose }
199256 aria-label = "Close alumni details"
200- className = "absolute top-4 right-4 inline-flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors cursor-pointer"
257+ className = "absolute top-4 right-4 sm:top-5 sm:right-5 inline-flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors cursor-pointer"
201258 >
202259 ✕
203260 </ button >
204261
205- < div className = "flex items-center gap-4 pr-8" >
206- < div className = "relative h-16 w-16 shrink-0 overflow-hidden rounded-xl bg-muted border border-border/60" >
262+ < div className = "flex items-center gap-4 sm:gap-5 pr-8" >
263+ < div className = "relative h-16 w-16 sm:h-18 sm:w-18 shrink-0 overflow-hidden rounded-xl bg-muted border border-border/60" >
207264 < Image
208265 src = { member . photo }
209266 alt = { member . name }
210267 fill
211268 className = "object-cover"
212- sizes = "64px"
269+ sizes = "(max-width: 640px) 64px, 72px "
213270 />
214271 </ div >
215272 < div className = "min-w-0" >
@@ -220,34 +277,34 @@ function AlumniDetailModal({
220277 { member . name }
221278 </ h3 >
222279 { member . role && (
223- < p className = "text-xs font-medium text-primary mt-0.5" >
280+ < p className = "text-xs sm:text-sm font-medium text-primary mt-0.5" >
224281 { member . role }
225282 </ p >
226283 ) }
227284 { member . title && (
228- < p className = "text-xs text-muted-foreground mt-0.5" >
285+ < p className = "text-xs sm:text-sm text-muted-foreground mt-0.5" >
229286 { member . title }
230287 </ p >
231288 ) }
232289 </ div >
233290 </ div >
234291
235292 { employmentList . length > 0 && (
236- < div className = "mt-5 pt-4 border-t border-border/60" >
237- < h4 className = "text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground mb-3" >
293+ < div className = "mt-6 pt-4 border-t border-border/60" >
294+ < h4 className = "text-[11px] sm:text-xs font-semibold uppercase tracking-[0.08em] text-muted-foreground mb-3.5 " >
238295 Career & Employment History
239296 </ h4 >
240- < ol className = "relative border-l border-border/80 ml-2 space-y-3" >
297+ < ol className = "relative border-l border-border/80 ml-2 space-y-3.5 " >
241298 { employmentList . map ( ( entry , idx ) => (
242- < li key = { entry } className = "pl-4 relative" >
299+ < li key = { entry } className = "pl-4 sm:pl-5 relative" >
243300 < span
244301 className = { `absolute -left-[5px] top-1.5 h-2.5 w-2.5 rounded-full border-2 border-card ${
245302 idx === 0 ? "bg-primary" : "bg-muted-foreground/50"
246303 } `}
247304 aria-hidden = "true"
248305 />
249306 < p
250- className = { `text-xs leading-relaxed ${
307+ className = { `text-xs sm:text-[13px] leading-relaxed ${
251308 idx === 0
252309 ? "font-medium text-foreground"
253310 : "text-muted-foreground"
@@ -261,30 +318,31 @@ function AlumniDetailModal({
261318 </ div >
262319 ) }
263320
264- < div className = "mt-5 pt-3 border-t border-border/60 flex items-center justify-end gap-2" >
321+ < div className = "mt-6 pt-3.5 border-t border-border/60 flex items-center justify-end gap-2.5 " >
265322 { member . url && (
266323 < a
267324 href = { member . url }
268325 target = "_blank"
269326 rel = "noopener noreferrer"
270- className = "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-primary/10 text-primary hover:bg-primary/15 transition-colors"
327+ className = "inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-xs sm:text-sm font-medium bg-primary/10 text-primary hover:bg-primary/15 transition-colors"
271328 >
272329 < span > Visit Website</ span >
273- < span className = "text-[11px] font-mono" aria-hidden = "true" >
330+ < span className = "text-[11px] sm:text-xs font-mono" aria-hidden = "true" >
274331 ↗
275332 </ span >
276333 </ a >
277334 ) }
278335 < button
279336 type = "button"
280337 onClick = { onClose }
281- className = "inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-muted text-foreground hover:bg-muted/80 transition-colors cursor-pointer"
338+ className = "inline-flex items-center px-3.5 py-1.5 rounded-lg text-xs sm:text-sm font-medium bg-muted text-foreground hover:bg-muted/80 transition-colors cursor-pointer"
282339 >
283340 Close
284341 </ button >
285342 </ div >
286343 </ div >
287- </ div >
344+ </ div > ,
345+ document . body ,
288346 ) ;
289347}
290348
0 commit comments