1
1
'use client' ;
2
2
3
3
import { useState } from 'react' ;
4
- import { X } from 'react-feather' ;
5
4
import * as Dialog from '@radix-ui/react-dialog' ;
6
5
import Image from 'next/image' ;
7
6
7
+ import { Lightbox } from 'sentry-docs/components/lightbox' ;
8
8
import { isAllowedRemoteImage } from 'sentry-docs/config/images' ;
9
9
10
- import styles from './imageLightbox.module.scss' ;
11
-
12
10
interface ImageLightboxProps
13
11
extends Omit <
14
12
React . HTMLProps < HTMLImageElement > ,
@@ -68,59 +66,58 @@ export function ImageLightbox({
68
66
const shouldUseNextImage =
69
67
! ! dimensions && ( ! isExternalImage ( src ) || isAllowedRemoteImage ( src ) ) ;
70
68
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
73
71
if ( e . ctrlKey || e . metaKey ) {
72
+ e . preventDefault ( ) ;
73
+ e . stopPropagation ( ) ;
74
74
const url = getImageUrl ( src , imgPath ) ;
75
75
const newWindow = window . open ( url , '_blank' ) ;
76
76
if ( newWindow ) {
77
77
newWindow . opener = null ; // Security: prevent opener access
78
78
}
79
- return ;
80
79
}
81
- // Normal click - open lightbox
82
- setOpen ( true ) ;
80
+ // Normal click will be handled by Dialog.Trigger
83
81
} ;
84
82
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 ) ) {
88
86
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
97
92
}
98
- // Normal key press - open lightbox
99
- setOpen ( true ) ;
100
93
}
94
+ // Normal key presses will be handled by Dialog.Trigger
101
95
} ;
102
96
103
97
// Filter out props that are incompatible with Next.js Image component
104
98
// Next.js Image has stricter typing for certain props like 'placeholder'
105
99
const { placeholder : _placeholder , ...imageCompatibleProps } = props ;
106
100
107
101
// Render the appropriate image component
108
- const renderImage = ( ) => {
102
+ const renderImage = ( isInline : boolean = true ) => {
109
103
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
+
110
111
if ( shouldUseNextImage && dimensions ) {
111
- // TypeScript knows validDimensions.width and validDimensions.height are both numbers
112
112
return (
113
113
< Image
114
114
src = { renderedSrc }
115
115
width = { dimensions . width }
116
116
height = { dimensions . height }
117
- style = { {
118
- width : '100%' ,
119
- height : 'auto' ,
120
- ...style ,
121
- } }
122
- className = { className }
117
+ style = { imageStyle }
118
+ className = { imageClassName }
123
119
alt = { alt }
120
+ priority = { ! isInline }
124
121
{ ...imageCompatibleProps }
125
122
/>
126
123
) ;
@@ -130,77 +127,27 @@ export function ImageLightbox({
130
127
< img
131
128
src = { renderedSrc }
132
129
alt = { alt }
133
- loading = " lazy"
130
+ loading = { isInline ? ' lazy' : 'lazy' }
134
131
decoding = "async"
135
- style = { {
136
- width : '100%' ,
137
- height : 'auto' ,
138
- ...style ,
139
- } }
140
- className = { className }
132
+ style = { imageStyle }
133
+ className = { imageClassName }
141
134
{ ...props }
142
135
/>
143
136
) ;
144
137
} ;
145
138
146
139
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 >
205
152
) ;
206
153
}
0 commit comments