@linktr.ee/messaging-react 2.3.0-rc-1779427772 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Card-DKp7ijLV.js → Card-4takoN_-.js} +6 -6
- package/dist/{Card-DKp7ijLV.js.map → Card-4takoN_-.js.map} +1 -1
- package/dist/{Card-Djm6JjNo.js → Card-BuROm0u7.js} +19 -19
- package/dist/{Card-Djm6JjNo.js.map → Card-BuROm0u7.js.map} +1 -1
- package/dist/{Card-BlzGsGam.cjs → Card-CexShqpK.cjs} +2 -2
- package/dist/{Card-BlzGsGam.cjs.map → Card-CexShqpK.cjs.map} +1 -1
- package/dist/{Card-BkWwtS0b.cjs → Card-CgpHBx-W.cjs} +2 -2
- package/dist/{Card-BkWwtS0b.cjs.map → Card-CgpHBx-W.cjs.map} +1 -1
- package/dist/{Card-B7yHs01-.js → Card-DdpdnSh_.js} +16 -16
- package/dist/{Card-B7yHs01-.js.map → Card-DdpdnSh_.js.map} +1 -1
- package/dist/{Card-DApWNv5V.cjs → Card-ot16XqS2.cjs} +2 -2
- package/dist/{Card-DApWNv5V.cjs.map → Card-ot16XqS2.cjs.map} +1 -1
- package/dist/{LockedThumbnail-BjF6khtg.cjs → LockedThumbnail-CydtYOSA.cjs} +2 -2
- package/dist/{LockedThumbnail-BjF6khtg.cjs.map → LockedThumbnail-CydtYOSA.cjs.map} +1 -1
- package/dist/{LockedThumbnail-pm6jo2B4.js → LockedThumbnail-Drsh4B5o.js} +8 -8
- package/dist/{LockedThumbnail-pm6jo2B4.js.map → LockedThumbnail-Drsh4B5o.js.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/index-BCbVXFHI.js +4698 -0
- package/dist/index-BCbVXFHI.js.map +1 -0
- package/dist/index-CQ913euH.cjs +2 -0
- package/dist/index-CQ913euH.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +22 -13
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/ChannelView.tsx +2 -8
- package/src/components/CustomMessage/CustomMessage.stories.tsx +0 -140
- package/src/components/CustomMessage/index.tsx +15 -20
- package/src/components/MessageAttachment/Image/ImageAttachment.stories.tsx +8 -5
- package/src/components/MessageAttachment/Image/index.tsx +7 -1
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +200 -19
- package/src/components/MessageAttachment/Pdf/index.tsx +14 -15
- package/src/components/MessageAttachment/Video/VideoAttachment.stories.tsx +2 -2
- package/src/components/MessageAttachment/Video/index.tsx +11 -2
- package/src/components/MessageAttachment/_shared/CarouselNav.tsx +47 -0
- package/src/components/MessageAttachment/_shared/DownloadAction.tsx +27 -27
- package/src/components/MessageAttachment/_shared/ImageViewer.tsx +59 -261
- package/src/components/MessageAttachment/_shared/PdfViewer.tsx +56 -30
- package/src/components/MessageAttachment/_shared/VideoViewer.tsx +53 -109
- package/src/components/MessageAttachment/_shared/ViewerShell.tsx +127 -107
- package/src/components/MessageAttachment/_shared/useCarousel.ts +103 -0
- package/src/components/MessageAttachment/index.tsx +18 -9
- package/src/components/MessageAttachment/types.ts +1 -1
- package/src/styles.css +177 -85
- package/dist/index-7sLuX6s4.cjs +0 -18
- package/dist/index-7sLuX6s4.cjs.map +0 -1
- package/dist/index-Co-LV7yc.js +0 -8220
- package/dist/index-Co-LV7yc.js.map +0 -1
- package/src/components/CustomMessage/CustomMessageActions.tsx +0 -35
|
@@ -1,28 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CaretLeftIcon,
|
|
3
|
-
CaretRightIcon,
|
|
4
|
-
MagnifyingGlassMinusIcon,
|
|
5
|
-
MagnifyingGlassPlusIcon,
|
|
6
|
-
} from '@phosphor-icons/react'
|
|
7
|
-
import classNames from 'classnames'
|
|
8
|
-
import React, {
|
|
9
|
-
useCallback,
|
|
10
|
-
useEffect,
|
|
11
|
-
useMemo,
|
|
12
|
-
useRef,
|
|
13
|
-
useState,
|
|
14
|
-
} from 'react'
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
15
2
|
|
|
3
|
+
import CarouselNav from './CarouselNav'
|
|
16
4
|
import DownloadAction from './DownloadAction'
|
|
17
5
|
import { filenameFromUrl } from './fileMeta'
|
|
6
|
+
import { useCarousel } from './useCarousel'
|
|
18
7
|
import ViewerShell from './ViewerShell'
|
|
19
8
|
|
|
20
9
|
export interface ImageViewerItem {
|
|
21
10
|
src: string
|
|
22
11
|
alt?: string
|
|
23
12
|
/**
|
|
24
|
-
* Filename used by the download action
|
|
25
|
-
* pathname segment of
|
|
13
|
+
* Filename used by the download action and as the dialog's
|
|
14
|
+
* accessible name. Falls back to the last pathname segment of
|
|
15
|
+
* `src` when omitted.
|
|
26
16
|
*/
|
|
27
17
|
filename?: string
|
|
28
18
|
}
|
|
@@ -30,34 +20,27 @@ export interface ImageViewerItem {
|
|
|
30
20
|
export interface ImageViewerProps {
|
|
31
21
|
open: boolean
|
|
32
22
|
items: ImageViewerItem[]
|
|
33
|
-
/**
|
|
23
|
+
/** Index to display when the viewer first opens. Defaults to `0`. */
|
|
34
24
|
initialIndex?: number
|
|
35
25
|
onClose: () => void
|
|
36
26
|
}
|
|
37
27
|
|
|
38
|
-
const MIN_SCALE = 1
|
|
39
|
-
const MAX_SCALE = 8
|
|
40
|
-
const ZOOM_STEP = 1.25
|
|
41
|
-
|
|
42
|
-
const clamp = (value: number, min: number, max: number) =>
|
|
43
|
-
Math.min(Math.max(value, min), max)
|
|
44
|
-
|
|
45
|
-
interface ViewportState {
|
|
46
|
-
scale: number
|
|
47
|
-
x: number
|
|
48
|
-
y: number
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const RESET_STATE: ViewportState = { scale: 1, x: 0, y: 0 }
|
|
52
|
-
|
|
53
28
|
/**
|
|
54
|
-
* Native lightbox-style image viewer
|
|
55
|
-
*
|
|
56
|
-
*
|
|
29
|
+
* Native lightbox-style image viewer — full-viewport `<dialog>` with
|
|
30
|
+
* the image centered at reasonable max dimensions, a single close
|
|
31
|
+
* button, and a download action in the chrome.
|
|
32
|
+
*
|
|
33
|
+
* When `items.length > 1` the viewer becomes a carousel:
|
|
57
34
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
35
|
+
* - Prev / next on-screen controls hug the viewport sides.
|
|
36
|
+
* - `ArrowLeft` / `ArrowRight` step between siblings (the keyboard
|
|
37
|
+
* listener is parked while a `<video>` / `<audio>` element is
|
|
38
|
+
* focused so native media seek still works in mixed contexts).
|
|
39
|
+
* - A `current / total` counter sits in the top-left chrome.
|
|
40
|
+
*
|
|
41
|
+
* Used by every `MessageAttachment.Image.*` variant
|
|
42
|
+
* (Composer / Sent / Received) so admins can preview attachments
|
|
43
|
+
* before sending and recipients can preview after receipt.
|
|
61
44
|
*/
|
|
62
45
|
const ImageViewer: React.FC<ImageViewerProps> = ({
|
|
63
46
|
open,
|
|
@@ -65,247 +48,62 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
|
|
|
65
48
|
initialIndex = 0,
|
|
66
49
|
onClose,
|
|
67
50
|
}) => {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
x: number
|
|
74
|
-
y: number
|
|
75
|
-
panX: number
|
|
76
|
-
panY: number
|
|
77
|
-
} | null>(null)
|
|
78
|
-
const stageRef = useRef<HTMLDivElement | null>(null)
|
|
79
|
-
|
|
80
|
-
// Reset zoom whenever the viewer opens or the active item changes.
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
if (!open) return
|
|
83
|
-
setIndex(clamp(initialIndex, 0, Math.max(items.length - 1, 0)))
|
|
84
|
-
setView(RESET_STATE)
|
|
85
|
-
}, [open, initialIndex, items.length])
|
|
86
|
-
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
setView(RESET_STATE)
|
|
89
|
-
}, [index])
|
|
90
|
-
|
|
91
|
-
const goPrev = useCallback(() => {
|
|
92
|
-
setIndex((i) => (i <= 0 ? items.length - 1 : i - 1))
|
|
93
|
-
}, [items.length])
|
|
94
|
-
|
|
95
|
-
const goNext = useCallback(() => {
|
|
96
|
-
setIndex((i) => (i >= items.length - 1 ? 0 : i + 1))
|
|
97
|
-
}, [items.length])
|
|
98
|
-
|
|
99
|
-
// Arrow-key navigation between items in the stack.
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
if (!open) return undefined
|
|
102
|
-
const onKey = (e: KeyboardEvent) => {
|
|
103
|
-
if (items.length <= 1) return
|
|
104
|
-
if (e.key === 'ArrowRight') {
|
|
105
|
-
e.preventDefault()
|
|
106
|
-
goNext()
|
|
107
|
-
} else if (e.key === 'ArrowLeft') {
|
|
108
|
-
e.preventDefault()
|
|
109
|
-
goPrev()
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
window.addEventListener('keydown', onKey)
|
|
113
|
-
return () => window.removeEventListener('keydown', onKey)
|
|
114
|
-
}, [open, items.length, goPrev, goNext])
|
|
115
|
-
|
|
116
|
-
const zoomBy = useCallback((factor: number) => {
|
|
117
|
-
setView((prev) => {
|
|
118
|
-
const nextScale = clamp(prev.scale * factor, MIN_SCALE, MAX_SCALE)
|
|
119
|
-
// Re-center when we hit 1× so the image is never offset off-screen.
|
|
120
|
-
if (nextScale === MIN_SCALE) return RESET_STATE
|
|
121
|
-
return { scale: nextScale, x: prev.x, y: prev.y }
|
|
122
|
-
})
|
|
123
|
-
}, [])
|
|
124
|
-
|
|
125
|
-
// React's synthetic `onWheel` is registered as a passive listener
|
|
126
|
-
// (since React 17), so `e.preventDefault()` is a no-op inside an
|
|
127
|
-
// `onWheel` handler — the page scrolls behind the viewer regardless.
|
|
128
|
-
// Attach the listener manually on the stage element with
|
|
129
|
-
// `{ passive: false }` so we can actually stop the page scroll
|
|
130
|
-
// while the user zooms.
|
|
131
|
-
useEffect(() => {
|
|
132
|
-
if (!open) return undefined
|
|
133
|
-
const stage = stageRef.current
|
|
134
|
-
if (!stage) return undefined
|
|
135
|
-
|
|
136
|
-
const handleWheel = (e: WheelEvent) => {
|
|
137
|
-
e.preventDefault()
|
|
138
|
-
const factor = e.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP
|
|
139
|
-
zoomBy(factor)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
stage.addEventListener('wheel', handleWheel, { passive: false })
|
|
143
|
-
return () => stage.removeEventListener('wheel', handleWheel)
|
|
144
|
-
}, [open, zoomBy])
|
|
145
|
-
|
|
146
|
-
const handleDoubleClick = useCallback(() => {
|
|
147
|
-
setView((prev) =>
|
|
148
|
-
prev.scale > 1
|
|
149
|
-
? RESET_STATE
|
|
150
|
-
: { scale: 2, x: prev.x, y: prev.y }
|
|
151
|
-
)
|
|
152
|
-
}, [])
|
|
153
|
-
|
|
154
|
-
const handleMouseDown = useCallback(
|
|
155
|
-
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
156
|
-
// Only allow drag-pan once the user has zoomed in.
|
|
157
|
-
if (view.scale <= 1) return
|
|
158
|
-
e.preventDefault()
|
|
159
|
-
setDragging(true)
|
|
160
|
-
dragStart.current = {
|
|
161
|
-
x: e.clientX,
|
|
162
|
-
y: e.clientY,
|
|
163
|
-
panX: view.x,
|
|
164
|
-
panY: view.y,
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
[view.scale, view.x, view.y]
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
useEffect(() => {
|
|
171
|
-
if (!dragging) return undefined
|
|
172
|
-
|
|
173
|
-
const onMove = (e: MouseEvent) => {
|
|
174
|
-
const start = dragStart.current
|
|
175
|
-
if (!start) return
|
|
176
|
-
setView((prev) => ({
|
|
177
|
-
scale: prev.scale,
|
|
178
|
-
x: start.panX + (e.clientX - start.x),
|
|
179
|
-
y: start.panY + (e.clientY - start.y),
|
|
180
|
-
}))
|
|
181
|
-
}
|
|
182
|
-
const onUp = () => {
|
|
183
|
-
dragStart.current = null
|
|
184
|
-
setDragging(false)
|
|
185
|
-
}
|
|
186
|
-
window.addEventListener('mousemove', onMove)
|
|
187
|
-
window.addEventListener('mouseup', onUp)
|
|
188
|
-
return () => {
|
|
189
|
-
window.removeEventListener('mousemove', onMove)
|
|
190
|
-
window.removeEventListener('mouseup', onUp)
|
|
191
|
-
}
|
|
192
|
-
}, [dragging])
|
|
51
|
+
const { index, prev, next } = useCarousel({
|
|
52
|
+
length: items.length,
|
|
53
|
+
initialIndex,
|
|
54
|
+
open,
|
|
55
|
+
})
|
|
193
56
|
|
|
194
57
|
const item = items[index]
|
|
195
58
|
const filename = useMemo(
|
|
196
|
-
() =>
|
|
197
|
-
item?.filename ?? (item ? filenameFromUrl(item.src) : 'image'),
|
|
59
|
+
() => item?.filename ?? (item ? filenameFromUrl(item.src) : 'image'),
|
|
198
60
|
[item]
|
|
199
61
|
)
|
|
200
62
|
|
|
201
|
-
const cursorClass = useMemo(() => {
|
|
202
|
-
if (view.scale <= 1) return 'cursor-zoom-in'
|
|
203
|
-
return dragging ? 'cursor-grabbing' : 'cursor-grab'
|
|
204
|
-
}, [view.scale, dragging])
|
|
205
|
-
|
|
206
63
|
if (!item) return null
|
|
207
64
|
|
|
208
|
-
const totalLabel = items.length > 1 ? ` (${index + 1} / ${items.length})` : ''
|
|
209
|
-
|
|
210
65
|
return (
|
|
211
66
|
<ViewerShell
|
|
212
67
|
open={open}
|
|
213
68
|
onClose={onClose}
|
|
214
|
-
|
|
69
|
+
ariaLabel={filename}
|
|
70
|
+
counter={
|
|
71
|
+
items.length > 1 ? `${index + 1} / ${items.length}` : undefined
|
|
72
|
+
}
|
|
215
73
|
actions={
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
className="flex size-10 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20 disabled:opacity-40"
|
|
223
|
-
>
|
|
224
|
-
<MagnifyingGlassMinusIcon
|
|
225
|
-
className="size-5"
|
|
226
|
-
weight="bold"
|
|
227
|
-
aria-hidden
|
|
228
|
-
/>
|
|
229
|
-
</button>
|
|
230
|
-
<button
|
|
231
|
-
type="button"
|
|
232
|
-
onClick={() => zoomBy(ZOOM_STEP)}
|
|
233
|
-
disabled={view.scale >= MAX_SCALE}
|
|
234
|
-
aria-label="Zoom in"
|
|
235
|
-
className="flex size-10 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20 disabled:opacity-40"
|
|
236
|
-
>
|
|
237
|
-
<MagnifyingGlassPlusIcon
|
|
238
|
-
className="size-5"
|
|
239
|
-
weight="bold"
|
|
240
|
-
aria-hidden
|
|
241
|
-
/>
|
|
242
|
-
</button>
|
|
243
|
-
<DownloadAction
|
|
244
|
-
url={item.src}
|
|
245
|
-
filename={filename}
|
|
246
|
-
variant="overlay"
|
|
247
|
-
label={`Download ${filename}`}
|
|
248
|
-
/>
|
|
249
|
-
</>
|
|
74
|
+
<DownloadAction
|
|
75
|
+
url={item.src}
|
|
76
|
+
filename={filename}
|
|
77
|
+
variant="viewer"
|
|
78
|
+
label={`Download ${filename}`}
|
|
79
|
+
/>
|
|
250
80
|
}
|
|
251
81
|
data-testid="image-viewer"
|
|
252
82
|
>
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
)}
|
|
270
|
-
>
|
|
271
|
-
<img
|
|
272
|
-
src={item.src}
|
|
273
|
-
alt={item.alt ?? filename}
|
|
274
|
-
draggable={false}
|
|
275
|
-
// Once the user has explicitly opened the viewer the active
|
|
276
|
-
// image needs to appear immediately, so we eager-load it.
|
|
277
|
-
// Adjacent / off-screen viewer images aren't rendered here
|
|
278
|
-
// (we only mount `items[index]`), so the lazy default on
|
|
279
|
-
// siblings is automatic.
|
|
280
|
-
loading="eager"
|
|
281
|
-
decoding="async"
|
|
282
|
-
style={{
|
|
283
|
-
transform: `translate3d(${view.x}px, ${view.y}px, 0) scale(${view.scale})`,
|
|
284
|
-
transition: dragging ? 'none' : 'transform 120ms ease-out',
|
|
285
|
-
}}
|
|
286
|
-
className="max-h-full max-w-full object-contain"
|
|
287
|
-
/>
|
|
288
|
-
</div>
|
|
83
|
+
<img
|
|
84
|
+
// Forcing a key swap on item change ensures React replaces the
|
|
85
|
+
// `<img>` cleanly between siblings — otherwise the previous
|
|
86
|
+
// image stays painted for a frame while the new `src`
|
|
87
|
+
// decodes, which reads as a stutter at carousel pace.
|
|
88
|
+
key={`${index}:${item.src}`}
|
|
89
|
+
src={item.src}
|
|
90
|
+
alt={item.alt ?? filename}
|
|
91
|
+
draggable={false}
|
|
92
|
+
// The user has explicitly opened the viewer, so we want the
|
|
93
|
+
// active image to appear immediately rather than fall to the
|
|
94
|
+
// browser's lazy-load heuristics.
|
|
95
|
+
loading="eager"
|
|
96
|
+
decoding="async"
|
|
97
|
+
className="mes-media-viewer__image"
|
|
98
|
+
/>
|
|
289
99
|
|
|
290
100
|
{items.length > 1 ? (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
>
|
|
298
|
-
<CaretLeftIcon className="size-5" weight="bold" aria-hidden />
|
|
299
|
-
</button>
|
|
300
|
-
<button
|
|
301
|
-
type="button"
|
|
302
|
-
onClick={goNext}
|
|
303
|
-
aria-label="Next image"
|
|
304
|
-
className="absolute right-4 top-1/2 z-10 flex size-12 -translate-y-1/2 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20"
|
|
305
|
-
>
|
|
306
|
-
<CaretRightIcon className="size-5" weight="bold" aria-hidden />
|
|
307
|
-
</button>
|
|
308
|
-
</>
|
|
101
|
+
<CarouselNav
|
|
102
|
+
onPrev={prev}
|
|
103
|
+
onNext={next}
|
|
104
|
+
prevLabel="Previous image"
|
|
105
|
+
nextLabel="Next image"
|
|
106
|
+
/>
|
|
309
107
|
) : null}
|
|
310
108
|
</ViewerShell>
|
|
311
109
|
)
|
|
@@ -1,67 +1,84 @@
|
|
|
1
1
|
import React, { useMemo } from 'react'
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import CarouselNav from './CarouselNav'
|
|
4
4
|
import { filenameFromUrl } from './fileMeta'
|
|
5
|
+
import { useCarousel } from './useCarousel'
|
|
5
6
|
import ViewerShell from './ViewerShell'
|
|
6
7
|
|
|
7
|
-
export interface
|
|
8
|
-
open: boolean
|
|
8
|
+
export interface PdfViewerItem {
|
|
9
9
|
/** Source URL of the PDF document. */
|
|
10
10
|
src: string
|
|
11
|
-
/** Filename used
|
|
11
|
+
/** Filename used as the dialog's accessible name. */
|
|
12
12
|
filename?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PdfViewerProps {
|
|
16
|
+
open: boolean
|
|
17
|
+
items: PdfViewerItem[]
|
|
18
|
+
/** Index to display when the viewer first opens. Defaults to `0`. */
|
|
19
|
+
initialIndex?: number
|
|
13
20
|
onClose: () => void
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
/**
|
|
17
|
-
* Modal PDF viewer that hosts the document inside a sandboxed
|
|
18
|
-
* `<iframe>`, leaving native
|
|
19
|
-
* browser
|
|
20
|
-
*
|
|
21
|
-
*
|
|
24
|
+
* Modal PDF viewer that hosts the active document inside a sandboxed
|
|
25
|
+
* `<iframe>`, leaving native rendering, scroll, search, and the
|
|
26
|
+
* browser's built-in download controls to the browser. The lightbox
|
|
27
|
+
* chrome is intentionally just a close button plus, for stacked
|
|
28
|
+
* attachments, carousel prev/next + counter.
|
|
22
29
|
*/
|
|
23
30
|
const PdfViewer: React.FC<PdfViewerProps> = ({
|
|
24
31
|
open,
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
items,
|
|
33
|
+
initialIndex = 0,
|
|
27
34
|
onClose,
|
|
28
35
|
}) => {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
const { index, prev, next } = useCarousel({
|
|
37
|
+
length: items.length,
|
|
38
|
+
initialIndex,
|
|
39
|
+
open,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const item = items[index]
|
|
43
|
+
const filename = useMemo(
|
|
44
|
+
() => item?.filename ?? (item ? filenameFromUrl(item.src) : 'document'),
|
|
45
|
+
[item]
|
|
32
46
|
)
|
|
33
47
|
|
|
34
48
|
// `#toolbar=0` is honored by Chromium-family browsers and hides the
|
|
35
|
-
// built-in PDF toolbar
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// the user gets a familiar experience either way.
|
|
49
|
+
// built-in PDF toolbar's redundant controls. Firefox / Safari ignore
|
|
50
|
+
// it and keep their native toolbars, which is fine — the user gets a
|
|
51
|
+
// familiar experience either way.
|
|
39
52
|
//
|
|
40
53
|
// Preserve any existing fragment the caller supplied (e.g.
|
|
41
54
|
// `#page=3`) by merging our params into it rather than naively
|
|
42
55
|
// appending a second `#…` segment.
|
|
43
|
-
const iframeSrc = useMemo(
|
|
56
|
+
const iframeSrc = useMemo(
|
|
57
|
+
() => (item ? withPdfViewerParams(item.src) : undefined),
|
|
58
|
+
[item]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if (!item || !iframeSrc) return null
|
|
44
62
|
|
|
45
63
|
return (
|
|
46
64
|
<ViewerShell
|
|
47
65
|
open={open}
|
|
48
66
|
onClose={onClose}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
url={src}
|
|
53
|
-
filename={resolvedFilename}
|
|
54
|
-
variant="overlay"
|
|
55
|
-
label={`Download ${resolvedFilename}`}
|
|
56
|
-
/>
|
|
67
|
+
ariaLabel={filename}
|
|
68
|
+
counter={
|
|
69
|
+
items.length > 1 ? `${index + 1} / ${items.length}` : undefined
|
|
57
70
|
}
|
|
58
|
-
contentClassName="bg-[#1f1e1d]"
|
|
59
71
|
data-testid="pdf-viewer"
|
|
60
72
|
>
|
|
61
73
|
<iframe
|
|
74
|
+
// Force a key swap on item change so the iframe remounts a
|
|
75
|
+
// fresh document rather than re-using the previous scroll
|
|
76
|
+
// position / search state when the user navigates between
|
|
77
|
+
// siblings.
|
|
78
|
+
key={`${index}:${item.src}`}
|
|
62
79
|
src={iframeSrc}
|
|
63
|
-
title={
|
|
64
|
-
className="
|
|
80
|
+
title={filename}
|
|
81
|
+
className="mes-media-viewer__iframe"
|
|
65
82
|
// Sandbox the iframe to stop the embedded document from
|
|
66
83
|
// reaching parent context. We intentionally omit
|
|
67
84
|
// `allow-same-origin`: even when the PDF host happens to share
|
|
@@ -74,6 +91,15 @@ const PdfViewer: React.FC<PdfViewerProps> = ({
|
|
|
74
91
|
// new tab.
|
|
75
92
|
sandbox="allow-scripts allow-forms allow-popups allow-downloads"
|
|
76
93
|
/>
|
|
94
|
+
|
|
95
|
+
{items.length > 1 ? (
|
|
96
|
+
<CarouselNav
|
|
97
|
+
onPrev={prev}
|
|
98
|
+
onNext={next}
|
|
99
|
+
prevLabel="Previous document"
|
|
100
|
+
nextLabel="Next document"
|
|
101
|
+
/>
|
|
102
|
+
) : null}
|
|
77
103
|
</ViewerShell>
|
|
78
104
|
)
|
|
79
105
|
}
|