11'use client' ;
22
33import { useState } from 'react' ;
4- import * as Dialog from '@radix-ui/react-dialog' ;
54import Image from 'next/image' ;
65
76import { Lightbox } from 'sentry-docs/components/lightbox' ;
8- import { isAllowedRemoteImage } from 'sentry-docs/config/images' ;
7+ import { isAllowedRemoteImage , isExternalImage } from 'sentry-docs/config/images' ;
98
109interface ImageLightboxProps
1110 extends Omit <
@@ -19,15 +18,9 @@ interface ImageLightboxProps
1918 width ?: number ;
2019}
2120
22- // Helper functions
23- const isExternalImage = ( src : string ) : boolean =>
24- src . startsWith ( 'http' ) || src . startsWith ( '//' ) ;
25-
2621const getImageUrl = ( src : string , imgPath : string ) : string =>
2722 isExternalImage ( src ) ? src : imgPath ;
2823
29- // Using shared allowlist logic from src/config/images
30-
3124type ValidDimensions = {
3225 height : number ;
3326 width : number ;
@@ -59,46 +52,36 @@ export function ImageLightbox({
5952} : ImageLightboxProps ) {
6053 const [ open , setOpen ] = useState ( false ) ;
6154
62- // Check if we should use Next.js Image or regular img
63- // Use Next.js Image for internal images with valid dimensions
64- // Use regular img for external images or when dimensions are invalid/missing
6555 const dimensions = getValidDimensions ( width , height ) ;
6656 const shouldUseNextImage =
6757 ! ! dimensions && ( ! isExternalImage ( src ) || isAllowedRemoteImage ( src ) ) ;
6858
69- const handleModifierClick = ( e : React . MouseEvent ) => {
70- // If Ctrl/Cmd+click, open image in new tab instead of lightbox
71- if ( e . ctrlKey || e . metaKey ) {
59+ const openInNewTab = ( ) => {
60+ window . open ( getImageUrl ( src , imgPath ) , '_blank' ) ;
61+ } ;
62+
63+ const handleClick = ( e : React . MouseEvent ) => {
64+ // Middle-click or Ctrl/Cmd+click opens in new tab
65+ if ( e . button === 1 || e . ctrlKey || e . metaKey ) {
7266 e . preventDefault ( ) ;
73- e . stopPropagation ( ) ;
74- const url = getImageUrl ( src , imgPath ) ;
75- const newWindow = window . open ( url , '_blank' ) ;
76- if ( newWindow ) {
77- newWindow . opener = null ; // Security: prevent opener access
78- }
67+ openInNewTab ( ) ;
68+ return ;
7969 }
80- // Normal click will be handled by Dialog.Trigger
70+ // Regular click falls through to Dialog.Trigger
8171 } ;
8272
83- const handleModifierKeyDown = ( e : React . KeyboardEvent ) => {
84- // Handle Ctrl/Cmd+Enter or Ctrl/Cmd+Space to open in new tab
73+ const handleKeyDown = ( e : React . KeyboardEvent ) => {
8574 if ( ( e . key === 'Enter' || e . key === ' ' ) && ( e . ctrlKey || e . metaKey ) ) {
8675 e . preventDefault ( ) ;
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
92- }
76+ openInNewTab ( ) ;
9377 }
94- // Normal key presses will be handled by Dialog.Trigger
78+ // Regular Enter/Space falls through to Dialog.Trigger
9579 } ;
9680
9781 // Filter out props that are incompatible with Next.js Image component
9882 // Next.js Image has stricter typing for certain props like 'placeholder'
9983 const { placeholder : _placeholder , ...imageCompatibleProps } = props ;
10084
101- // Render the appropriate image component
10285 const renderImage = ( isInline : boolean = true ) => {
10386 const renderedSrc = getImageUrl ( src , imgPath ) ;
10487 const imageClassName = isInline
@@ -127,27 +110,30 @@ export function ImageLightbox({
127110 < img
128111 src = { renderedSrc }
129112 alt = { alt }
130- loading = { isInline ? 'lazy' : 'lazy ' }
113+ loading = { isInline ? 'lazy' : 'eager ' }
131114 decoding = "async"
132115 style = { imageStyle }
133116 className = { imageClassName }
134- { ...props }
117+ { ...imageCompatibleProps }
135118 />
136119 ) ;
137120 } ;
138121
139122 return (
140123 < Lightbox . Root open = { open } onOpenChange = { setOpen } content = { renderImage ( false ) } >
141- < Dialog . Trigger asChild >
124+ < Lightbox . Trigger asChild >
142125 < div
143- onClick = { handleModifierClick }
144- onKeyDown = { handleModifierKeyDown }
126+ onClick = { handleClick }
127+ onAuxClick = { handleClick }
128+ onKeyDown = { handleKeyDown }
145129 className = "cursor-pointer border-none bg-transparent p-0 block w-full no-underline"
146130 aria-label = { `View image: ${ alt } ` }
131+ role = "button"
132+ tabIndex = { 0 }
147133 >
148134 { renderImage ( ) }
149135 </ div >
150- </ Dialog . Trigger >
136+ </ Lightbox . Trigger >
151137 </ Lightbox . Root >
152138 ) ;
153139}
0 commit comments