| ... | @@ -1,4 +1,12 @@ | ... | @@ -1,4 +1,12 @@ |
| 1 | import { memo, useCallback, useEffect, useRef, useState, type ReactNode } from "react"; | 1 | import { |
| | 2 | memo, |
| | 3 | useCallback, |
| | 4 | useEffect, |
| | 5 | useRef, |
| | 6 | useState, |
| | 7 | type ReactNode, |
| | 8 | type KeyboardEvent, |
| | 9 | } from "react"; |
| 2 | import { ChevronLeftIcon, ChevronRightIcon, ImageIcon, TextIcon, XIcon } from "lucide-react"; | 10 | import { ChevronLeftIcon, ChevronRightIcon, ImageIcon, TextIcon, XIcon } from "lucide-react"; |
| 3 | import { Button } from "../ui/button"; | 11 | import { Button } from "../ui/button"; |
| 4 | import { Dialog, DialogPopup, DialogTitle } from "../ui/dialog"; | 12 | import { Dialog, DialogPopup, DialogTitle } from "../ui/dialog"; |
| ... | @@ -24,7 +32,7 @@ interface ExpandedImageDialogProps { | ... | @@ -24,7 +32,7 @@ interface ExpandedImageDialogProps { |
| 24 | } | 32 | } |
| 25 | | 33 | |
| 26 | const EXPANDED_MEDIA_STATE_CLASS_NAME = | 34 | const EXPANDED_MEDIA_STATE_CLASS_NAME = |
| 27 | "flex aspect-auto h-48 min-h-0 w-[min(92vw,32rem)] flex-col items-center justify-center gap-3 rounded-lg border border-border/70 bg-black p-6 text-center text-sm text-white shadow-2xl"; | 35 | "flex aspect-auto h-48 min-h-0 w-[min(var(--media-width),32rem)] flex-col items-center justify-center gap-3 rounded-lg border border-border/70 bg-black p-6 text-center text-sm text-white shadow-2xl"; |
| 28 | | 36 | |
| 29 | function ExpandedMediaFailure({ children }: { children: ReactNode }) { | 37 | function ExpandedMediaFailure({ children }: { children: ReactNode }) { |
| 30 | return ( | 38 | return ( |
| ... | @@ -51,8 +59,8 @@ function ExpandedVideo({ item }: { readonly item: ExpandedImageItem }) { | ... | @@ -51,8 +59,8 @@ function ExpandedVideo({ item }: { readonly item: ExpandedImageItem }) { |
| 51 | originalUrl={item.originalUrl} | 59 | originalUrl={item.originalUrl} |
| 52 | preload="metadata" | 60 | preload="metadata" |
| 53 | autoPlay={item.autoPlay ?? true} | 61 | autoPlay={item.autoPlay ?? true} |
| 54 | className="block max-h-[86vh] max-w-[92vw] text-center" | 62 | className="block max-h-[var(--media-height)] max-w-[var(--media-width)] text-center" |
| 55 | videoClassName="aspect-auto max-h-[86vh] w-auto max-w-[92vw] rounded-lg border border-border/70 shadow-2xl" | 63 | videoClassName="aspect-auto max-h-[var(--media-height)] w-auto max-w-[var(--media-width)] rounded-lg border border-border/70 shadow-2xl" |
| 56 | stateClassName={EXPANDED_MEDIA_STATE_CLASS_NAME} | 64 | stateClassName={EXPANDED_MEDIA_STATE_CLASS_NAME} |
| 57 | onRetry={asset ? refreshAssetUrl : undefined} | 65 | onRetry={asset ? refreshAssetUrl : undefined} |
| 58 | /> | 66 | /> |
| ... | @@ -111,29 +119,26 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ | ... | @@ -111,29 +119,26 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ |
| 111 | }; | 119 | }; |
| 112 | }, []); | 120 | }, []); |
| 113 | | 121 | |
| 114 | useEffect(() => { | 122 | const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => { |
| 115 | const onKeyDown = (event: globalThis.KeyboardEvent) => { | 123 | if (event.defaultPrevented || isContextMenuOpen() || event.target instanceof HTMLVideoElement) |
| 116 | if (event.defaultPrevented || isContextMenuOpen()) return; | 124 | return; |
| 117 | if (zoomableImageRef.current?.pan(event.key)) { | 125 | if (zoomableImageRef.current?.pan(event.key)) { |
| 118 | event.preventDefault(); | | |
| 119 | event.stopPropagation(); | | |
| 120 | return; | | |
| 121 | } | | |
| 122 | if (preview.images.length <= 1) return; | | |
| 123 | if (event.key === "ArrowLeft") { | | |
| 124 | event.preventDefault(); | | |
| 125 | event.stopPropagation(); | | |
| 126 | navigateImage(-1); | | |
| 127 | return; | | |
| 128 | } | | |
| 129 | if (event.key !== "ArrowRight") return; | | |
| 130 | event.preventDefault(); | 126 | event.preventDefault(); |
| 131 | event.stopPropagation(); | 127 | event.stopPropagation(); |
| 132 | navigateImage(1); | 128 | return; |
| 133 | }; | 129 | } |
| 134 | window.addEventListener("keydown", onKeyDown); | 130 | if (preview.images.length <= 1) return; |
| 135 | return () => window.removeEventListener("keydown", onKeyDown); | 131 | if (event.key === "ArrowLeft") { |
| 136 | }, [navigateImage, preview.images.length]); | 132 | event.preventDefault(); |
| | 133 | event.stopPropagation(); |
| | 134 | navigateImage(-1); |
| | 135 | return; |
| | 136 | } |
| | 137 | if (event.key !== "ArrowRight") return; |
| | 138 | event.preventDefault(); |
| | 139 | event.stopPropagation(); |
| | 140 | navigateImage(1); |
| | 141 | }; |
| 137 | | 142 | |
| 138 | useEffect(() => { | 143 | useEffect(() => { |
| 139 | const onEscape = (event: globalThis.KeyboardEvent) => { | 144 | const onEscape = (event: globalThis.KeyboardEvent) => { |
| ... | @@ -176,9 +181,13 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ | ... | @@ -176,9 +181,13 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ |
| 176 | bottomStickOnMobile={false} | 181 | bottomStickOnMobile={false} |
| 177 | backdropClassName="z-[60]" | 182 | backdropClassName="z-[60]" |
| 178 | viewportClassName="z-[60] grid-rows-1 place-items-center px-4 py-6 [-webkit-app-region:no-drag]" | 183 | viewportClassName="z-[60] grid-rows-1 place-items-center px-4 py-6 [-webkit-app-region:no-drag]" |
| 179 | className="row-start-1 max-h-[92vh] w-auto max-w-[92vw] overflow-visible" | 184 | className="row-start-1 max-h-[92vh] w-[92vw] max-w-[92vw] items-center overflow-visible [--media-width:92vw] [--media-height:min(86vh,calc(100vh-160px))] sm:[--media-width:calc(92vw-96px)]" |
| | 185 | onKeyDown={onKeyDown} |
| 180 | initialFocus={closeButtonRef} | 186 | initialFocus={closeButtonRef} |
| 181 | finalFocus={() => returnFocusTarget} | 187 | finalFocus={() => returnFocusTarget} |
| | 188 | onClick={(event) => { |
| | 189 | if (event.target === event.currentTarget) onClose(); |
| | 190 | }} |
| 182 | > | 191 | > |
| 183 | <DialogTitle className="sr-only">Expanded {mediaLabel} preview</DialogTitle> | 192 | <DialogTitle className="sr-only">Expanded {mediaLabel} preview</DialogTitle> |
| 184 | {preview.images.length > 1 && ( | 193 | {preview.images.length > 1 && ( |
| ... | @@ -186,7 +195,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ | ... | @@ -186,7 +195,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ |
| 186 | type="button" | 195 | type="button" |
| 187 | size="icon" | 196 | size="icon" |
| 188 | variant="media-navigation" | 197 | variant="media-navigation" |
| 189 | className="left-2 sm:left-6" | 198 | className="left-0 top-auto -bottom-12 translate-y-0 rounded-full bg-white/10 sm:top-1/2 sm:bottom-auto sm:-translate-y-1/2" |
| 190 | aria-label="Previous media" | 199 | aria-label="Previous media" |
| 191 | onClick={() => navigateImage(-1)} | 200 | onClick={() => navigateImage(-1)} |
| 192 | > | 201 | > |
| ... | @@ -194,13 +203,13 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ | ... | @@ -194,13 +203,13 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ |
| 194 | </Button> | 203 | </Button> |
| 195 | )} | 204 | )} |
| 196 | <MediaActions source={actionsSource}> | 205 | <MediaActions source={actionsSource}> |
| 197 | <div className="relative isolate z-10 max-h-[92vh] max-w-[92vw]"> | 206 | <div className="relative isolate z-10 max-h-[92vh] max-w-[var(--media-width)]"> |
| 198 | <Button | 207 | <Button |
| 199 | type="button" | 208 | type="button" |
| 200 | ref={closeButtonRef} | 209 | ref={closeButtonRef} |
| 201 | size="icon-xs" | 210 | size="icon-xs" |
| 202 | variant="media-close" | 211 | variant="media-close" |
| 203 | className="absolute right-2 top-2 z-20" | 212 | className="absolute right-0 -top-10 z-20" |
| 204 | onClick={onClose} | 213 | onClick={onClose} |
| 205 | aria-label={`Close ${mediaLabel} preview`} | 214 | aria-label={`Close ${mediaLabel} preview`} |
| 206 | > | 215 | > |
| ... | @@ -212,7 +221,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ | ... | @@ -212,7 +221,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ |
| 212 | accessibilityDetails ? ( | 221 | accessibilityDetails ? ( |
| 213 | <SnapShotAccessibilityData | 222 | <SnapShotAccessibilityData |
| 214 | details={accessibilityDetails} | 223 | details={accessibilityDetails} |
| 215 | className="h-[min(86vh,40rem)] w-[min(92vw,42rem)] animate-[snap-shot-contents-enter_140ms_ease-out] rounded-lg border border-border/70 bg-background p-4 text-xs leading-5 shadow-2xl motion-reduce:animate-none" | 224 | className="h-[min(var(--media-height),40rem)] w-[min(var(--media-width),42rem)] animate-[snap-shot-contents-enter_140ms_ease-out] rounded-lg border border-border/70 bg-background p-4 text-xs leading-5 shadow-2xl motion-reduce:animate-none" |
| 216 | /> | 225 | /> |
| 217 | ) : null | 226 | ) : null |
| 218 | ) : item.src === null || failedImageSrc === item.src ? ( | 227 | ) : item.src === null || failedImageSrc === item.src ? ( |
| ... | @@ -233,8 +242,8 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ | ... | @@ -233,8 +242,8 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ |
| 233 | onError={() => setFailedImageSrc(item.src)} | 242 | onError={() => setFailedImageSrc(item.src)} |
| 234 | /> | 243 | /> |
| 235 | )} | 244 | )} |
| 236 | <div className="mt-2 flex max-w-[92vw] items-center justify-center gap-1.5 text-xs text-white/80"> | 245 | <div className="mt-2 flex max-w-[var(--media-width)] items-center justify-center gap-1.5 text-xs text-white/80"> |
| 237 | <span className="truncate"> | 246 | <span className="truncate" aria-live="polite" aria-atomic="true"> |
| 238 | {item.name} | 247 | {item.name} |
| 239 | {preview.images.length > 1 ? ` (${index + 1}/${preview.images.length})` : ""} | 248 | {preview.images.length > 1 ? ` (${index + 1}/${preview.images.length})` : ""} |
| 240 | </span> | 249 | </span> |
| ... | @@ -273,7 +282,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ | ... | @@ -273,7 +282,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ |
| 273 | type="button" | 282 | type="button" |
| 274 | size="icon" | 283 | size="icon" |
| 275 | variant="media-navigation" | 284 | variant="media-navigation" |
| 276 | className="right-2 sm:right-6" | 285 | className="right-0 top-auto -bottom-12 translate-y-0 rounded-full bg-white/10 sm:top-1/2 sm:bottom-auto sm:-translate-y-1/2" |
| 277 | aria-label="Next media" | 286 | aria-label="Next media" |
| 278 | onClick={() => navigateImage(1)} | 287 | onClick={() => navigateImage(1)} |
| 279 | > | 288 | > |