| ... | ... | @@ -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 | 10 | import { ChevronLeftIcon, ChevronRightIcon, ImageIcon, TextIcon, XIcon } from "lucide-react"; |
| 3 | 11 | import { Button } from "../ui/button"; |
| 4 | 12 | import { Dialog, DialogPopup, DialogTitle } from "../ui/dialog"; |
| ... | ... | @@ -24,7 +32,7 @@ interface ExpandedImageDialogProps { |
| 24 | 32 | } |
| 25 | 33 | |
| 26 | 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 | 37 | function ExpandedMediaFailure({ children }: { children: ReactNode }) { |
| 30 | 38 | return ( |
| ... | ... | @@ -51,8 +59,8 @@ function ExpandedVideo({ item }: { readonly item: ExpandedImageItem }) { |
| 51 | 59 | originalUrl={item.originalUrl} |
| 52 | 60 | preload="metadata" |
| 53 | 61 | autoPlay={item.autoPlay ?? true} |
| 54 | | className="block max-h-[86vh] max-w-[92vw] text-center" |
| 55 | | videoClassName="aspect-auto max-h-[86vh] w-auto max-w-[92vw] rounded-lg border border-border/70 shadow-2xl" |
| 62 | className="block max-h-[var(--media-height)] max-w-[var(--media-width)] text-center" |
| 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 | 64 | stateClassName={EXPANDED_MEDIA_STATE_CLASS_NAME} |
| 57 | 65 | onRetry={asset ? refreshAssetUrl : undefined} |
| 58 | 66 | /> |
| ... | ... | @@ -111,29 +119,26 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ |
| 111 | 119 | }; |
| 112 | 120 | }, []); |
| 113 | 121 | |
| 114 | | useEffect(() => { |
| 115 | | const onKeyDown = (event: globalThis.KeyboardEvent) => { |
| 116 | | if (event.defaultPrevented || isContextMenuOpen()) return; |
| 117 | | 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; |
| 122 | const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => { |
| 123 | if (event.defaultPrevented || isContextMenuOpen() || event.target instanceof HTMLVideoElement) |
| 124 | return; |
| 125 | if (zoomableImageRef.current?.pan(event.key)) { |
| 130 | 126 | event.preventDefault(); |
| 131 | 127 | event.stopPropagation(); |
| 132 | | navigateImage(1); |
| 133 | | }; |
| 134 | | window.addEventListener("keydown", onKeyDown); |
| 135 | | return () => window.removeEventListener("keydown", onKeyDown); |
| 136 | | }, [navigateImage, preview.images.length]); |
| 128 | return; |
| 129 | } |
| 130 | if (preview.images.length <= 1) return; |
| 131 | if (event.key === "ArrowLeft") { |
| 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 | 143 | useEffect(() => { |
| 139 | 144 | const onEscape = (event: globalThis.KeyboardEvent) => { |
| ... | ... | @@ -176,9 +181,13 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ |
| 176 | 181 | bottomStickOnMobile={false} |
| 177 | 182 | backdropClassName="z-[60]" |
| 178 | 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 | 186 | initialFocus={closeButtonRef} |
| 181 | 187 | finalFocus={() => returnFocusTarget} |
| 188 | onClick={(event) => { |
| 189 | if (event.target === event.currentTarget) onClose(); |
| 190 | }} |
| 182 | 191 | > |
| 183 | 192 | <DialogTitle className="sr-only">Expanded {mediaLabel} preview</DialogTitle> |
| 184 | 193 | {preview.images.length > 1 && ( |
| ... | ... | @@ -186,7 +195,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ |
| 186 | 195 | type="button" |
| 187 | 196 | size="icon" |
| 188 | 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 | 199 | aria-label="Previous media" |
| 191 | 200 | onClick={() => navigateImage(-1)} |
| 192 | 201 | > |
| ... | ... | @@ -194,13 +203,13 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ |
| 194 | 203 | </Button> |
| 195 | 204 | )} |
| 196 | 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 | 207 | <Button |
| 199 | 208 | type="button" |
| 200 | 209 | ref={closeButtonRef} |
| 201 | 210 | size="icon-xs" |
| 202 | 211 | variant="media-close" |
| 203 | | className="absolute right-2 top-2 z-20" |
| 212 | className="absolute right-0 -top-10 z-20" |
| 204 | 213 | onClick={onClose} |
| 205 | 214 | aria-label={`Close ${mediaLabel} preview`} |
| 206 | 215 | > |
| ... | ... | @@ -212,7 +221,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ |
| 212 | 221 | accessibilityDetails ? ( |
| 213 | 222 | <SnapShotAccessibilityData |
| 214 | 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 | 226 | ) : null |
| 218 | 227 | ) : item.src === null || failedImageSrc === item.src ? ( |
| ... | ... | @@ -233,8 +242,8 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ |
| 233 | 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"> |
| 237 | | <span className="truncate"> |
| 245 | <div className="mt-2 flex max-w-[var(--media-width)] items-center justify-center gap-1.5 text-xs text-white/80"> |
| 246 | <span className="truncate" aria-live="polite" aria-atomic="true"> |
| 238 | 247 | {item.name} |
| 239 | 248 | {preview.images.length > 1 ? ` (${index + 1}/${preview.images.length})` : ""} |
| 240 | 249 | </span> |
| ... | ... | @@ -273,7 +282,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ |
| 273 | 282 | type="button" |
| 274 | 283 | size="icon" |
| 275 | 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 | 286 | aria-label="Next media" |
| 278 | 287 | onClick={() => navigateImage(1)} |
| 279 | 288 | > |