11'use client' ;
22
33import { useState } from 'react' ;
4- import { X } from 'react-feather' ;
54import * as Dialog from '@radix-ui/react-dialog' ;
65import Image from 'next/image' ;
76
7+ import { Lightbox } from 'sentry-docs/components/lightbox' ;
88import { isAllowedRemoteImage } from 'sentry-docs/config/images' ;
99
10- import styles from './imageLightbox.module.scss' ;
11-
1210interface ImageLightboxProps
1311 extends Omit <
1412 React . HTMLProps < HTMLImageElement > ,
@@ -68,59 +66,58 @@ export function ImageLightbox({
6866 const shouldUseNextImage =
6967 ! ! dimensions && ( ! isExternalImage ( src ) || isAllowedRemoteImage ( src ) ) ;
7068
71- const handleClick = ( e : React . MouseEvent ) => {
72- // If Ctrl/Cmd+click, open image in new tab
69+ const handleModifierClick = ( e : React . MouseEvent ) => {
70+ // If Ctrl/Cmd+click, open image in new tab instead of lightbox
7371 if ( e . ctrlKey || e . metaKey ) {
72+ e . preventDefault ( ) ;
73+ e . stopPropagation ( ) ;
7474 const url = getImageUrl ( src , imgPath ) ;
7575 const newWindow = window . open ( url , '_blank' ) ;
7676 if ( newWindow ) {
7777 newWindow . opener = null ; // Security: prevent opener access
7878 }
79- return ;
8079 }
81- // Normal click - open lightbox
82- setOpen ( true ) ;
80+ // Normal click will be handled by Dialog.Trigger
8381 } ;
8482
85- const handleKeyDown = ( e : React . KeyboardEvent ) => {
86- // Handle Enter and Space keys
87- if ( e . key === 'Enter' || e . key === ' ' ) {
83+ const handleModifierKeyDown = ( e : React . KeyboardEvent ) => {
84+ // Handle Ctrl/Cmd+ Enter or Ctrl/Cmd+ Space to open in new tab
85+ if ( ( e . key === 'Enter' || e . key === ' ' ) && ( e . ctrlKey || e . metaKey ) ) {
8886 e . preventDefault ( ) ;
89- // If Ctrl/Cmd+key, open image in new tab
90- if ( e . ctrlKey || e . metaKey ) {
91- const url = getImageUrl ( src , imgPath ) ;
92- const newWindow = window . open ( url , '_blank' ) ;
93- if ( newWindow ) {
94- newWindow . opener = null ; // Security: prevent opener access
95- }
96- return ;
87+ e . stopPropagation ( ) ;
88+ const url = getImageUrl ( src , imgPath ) ;
89+ const newWindow = window . open ( url , '_blank' ) ;
90+ if ( newWindow ) {
91+ newWindow . opener = null ; // Security: prevent opener access
9792 }
98- // Normal key press - open lightbox
99- setOpen ( true ) ;
10093 }
94+ // Normal key presses will be handled by Dialog.Trigger
10195 } ;
10296
10397 // Filter out props that are incompatible with Next.js Image component
10498 // Next.js Image has stricter typing for certain props like 'placeholder'
10599 const { placeholder : _placeholder , ...imageCompatibleProps } = props ;
106100
107101 // Render the appropriate image component
108- const renderImage = ( ) => {
102+ const renderImage = ( isInline : boolean = true ) => {
109103 const renderedSrc = getImageUrl ( src , imgPath ) ;
104+ const imageClassName = isInline
105+ ? className
106+ : 'max-h-[90vh] max-w-[90vw] object-contain' ;
107+ const imageStyle = isInline
108+ ? { width : '100%' , height : 'auto' , ...style }
109+ : { width : 'auto' , height : 'auto' } ;
110+
110111 if ( shouldUseNextImage && dimensions ) {
111- // TypeScript knows validDimensions.width and validDimensions.height are both numbers
112112 return (
113113 < Image
114114 src = { renderedSrc }
115115 width = { dimensions . width }
116116 height = { dimensions . height }
117- style = { {
118- width : '100%' ,
119- height : 'auto' ,
120- ...style ,
121- } }
122- className = { className }
117+ style = { imageStyle }
118+ className = { imageClassName }
123119 alt = { alt }
120+ priority = { ! isInline }
124121 { ...imageCompatibleProps }
125122 />
126123 ) ;
@@ -130,77 +127,27 @@ export function ImageLightbox({
130127 < img
131128 src = { renderedSrc }
132129 alt = { alt }
133- loading = " lazy"
130+ loading = { isInline ? ' lazy' : 'lazy' }
134131 decoding = "async"
135- style = { {
136- width : '100%' ,
137- height : 'auto' ,
138- ...style ,
139- } }
140- className = { className }
132+ style = { imageStyle }
133+ className = { imageClassName }
141134 { ...props }
142135 />
143136 ) ;
144137 } ;
145138
146139 return (
147- < Dialog . Root open = { open } onOpenChange = { setOpen } >
148- { /* Custom trigger that handles modifier keys properly */ }
149- < div
150- role = "button"
151- tabIndex = { 0 }
152- className = "cursor-pointer border-none bg-transparent p-0 block w-full no-underline"
153- onClick = { handleClick }
154- onKeyDown = { handleKeyDown }
155- aria-label = { `View image: ${ alt } ` }
156- >
157- { renderImage ( ) }
158- </ div >
159-
160- < Dialog . Portal >
161- < Dialog . Overlay className = { styles . lightboxOverlay } />
162-
163- < Dialog . Content className = { styles . lightboxContent } >
164- { /* Close button */ }
165- < Dialog . Close className = "absolute right-4 top-4 z-10 rounded-sm bg-[var(--flame0)] border border-white/20 p-2 text-white opacity-90 transition-all duration-200 hover:opacity-100 hover:bg-[var(--flame1)] hover:border-white/30 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-white active:scale-95 flex items-center justify-center" >
166- < X className = "h-4 w-4 stroke-[2.5]" />
167- < span className = "sr-only" > Close</ span >
168- </ Dialog . Close >
169-
170- { /* Image container */ }
171- < div className = "relative flex items-center justify-center" >
172- { shouldUseNextImage && dimensions ? (
173- < Image
174- src = { getImageUrl ( src , imgPath ) }
175- alt = { alt }
176- width = { dimensions . width }
177- height = { dimensions . height }
178- className = "max-h-[90vh] max-w-[90vw] object-contain"
179- style = { {
180- width : 'auto' ,
181- height : 'auto' ,
182- } }
183- priority
184- { ...imageCompatibleProps }
185- />
186- ) : (
187- /* eslint-disable-next-line @next/next/no-img-element */
188- < img
189- src = { getImageUrl ( src , imgPath ) }
190- alt = { alt }
191- loading = "lazy"
192- decoding = "async"
193- className = "max-h-[90vh] max-w-[90vw] object-contain"
194- style = { {
195- width : 'auto' ,
196- height : 'auto' ,
197- } }
198- { ...props }
199- />
200- ) }
201- </ div >
202- </ Dialog . Content >
203- </ Dialog . Portal >
204- </ Dialog . Root >
140+ < Lightbox . Root open = { open } onOpenChange = { setOpen } content = { renderImage ( false ) } >
141+ < Dialog . Trigger asChild >
142+ < div
143+ onClick = { handleModifierClick }
144+ onKeyDown = { handleModifierKeyDown }
145+ className = "cursor-pointer border-none bg-transparent p-0 block w-full no-underline"
146+ aria-label = { `View image: ${ alt } ` }
147+ >
148+ { renderImage ( ) }
149+ </ div >
150+ </ Dialog . Trigger >
151+ </ Lightbox . Root >
205152 ) ;
206153}
0 commit comments