fix(ui): MediaGallery lightbox uses folds Overlay + FocusTrap (native-cinny audit 8/N)
The full-screen media viewer was a raw <div role="dialog"> rendered in place with manual focus. Wrapped it in folds Overlay (portal + backdrop, proper stacking) and FocusTrap (focus management), keeping its own arrow/Escape key handling. The light-on-dark chrome (#fff over the forced-black media stage) is kept — it's a justified, always-dark media-viewer scrim, not theme chrome. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,8 @@ import {
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
Scroll,
|
||||
Spinner,
|
||||
Text,
|
||||
@@ -15,6 +17,7 @@ import {
|
||||
config,
|
||||
} from 'folds';
|
||||
import { EventType, MatrixClient, MatrixEvent, MsgType, Room } from 'matrix-js-sdk';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import classNames from 'classnames';
|
||||
import { useNearViewport } from '../../hooks/useNearViewport';
|
||||
import { IEncryptedFile, IImageInfo, IThumbnailContent } from '../../../types/matrix/common';
|
||||
@@ -250,102 +253,112 @@ function Lightbox({
|
||||
});
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal
|
||||
aria-label="Media viewer"
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 1000,
|
||||
background: 'rgba(0,0,0,0.92)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{/* Header bar */}
|
||||
<Box
|
||||
alignItems="Center"
|
||||
gap="200"
|
||||
style={{
|
||||
padding: `${config.space.S200} ${config.space.S300}`,
|
||||
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
||||
flexShrink: 0,
|
||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
clickOutsideDeactivates: false,
|
||||
escapeDeactivates: false,
|
||||
}}
|
||||
>
|
||||
<Box grow="Yes" direction="Column" style={{ overflow: 'hidden' }}>
|
||||
<Text size="T400" truncate style={{ color: '#fff', fontWeight: 500 }}>
|
||||
{item.body || (item.msgtype === MsgType.Video ? 'Video' : 'Image')}
|
||||
</Text>
|
||||
<Text size="T200" style={{ color: 'rgba(255,255,255,0.5)' }}>
|
||||
{item.sender} · {dateStr}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text size="T200" style={{ color: 'rgba(255,255,255,0.4)', flexShrink: 0 }}>
|
||||
{index + 1} / {items.length}
|
||||
</Text>
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
align="End"
|
||||
offset={4}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text>Close</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal
|
||||
aria-label="Media viewer"
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 1000,
|
||||
background: 'rgba(0,0,0,0.92)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{(ref) => (
|
||||
<IconButton ref={ref} variant="Surface" aria-label="Close" onClick={onClose}>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
</Box>
|
||||
{/* Header bar */}
|
||||
<Box
|
||||
alignItems="Center"
|
||||
gap="200"
|
||||
style={{
|
||||
padding: `${config.space.S200} ${config.space.S300}`,
|
||||
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Box grow="Yes" direction="Column" style={{ overflow: 'hidden' }}>
|
||||
<Text size="T400" truncate style={{ color: '#fff', fontWeight: 500 }}>
|
||||
{item.body || (item.msgtype === MsgType.Video ? 'Video' : 'Image')}
|
||||
</Text>
|
||||
<Text size="T200" style={{ color: 'rgba(255,255,255,0.5)' }}>
|
||||
{item.sender} · {dateStr}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text size="T200" style={{ color: 'rgba(255,255,255,0.4)', flexShrink: 0 }}>
|
||||
{index + 1} / {items.length}
|
||||
</Text>
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
align="End"
|
||||
offset={4}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text>Close</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(ref) => (
|
||||
<IconButton ref={ref} variant="Surface" aria-label="Close" onClick={onClose}>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
</Box>
|
||||
|
||||
{/* Media area with nav arrows */}
|
||||
<Box
|
||||
grow="Yes"
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
style={{ overflow: 'hidden', padding: config.space.S400 }}
|
||||
>
|
||||
{index > 0 && (
|
||||
<IconButton
|
||||
variant="Surface"
|
||||
aria-label="Previous"
|
||||
onClick={prev}
|
||||
style={{ flexShrink: 0, marginRight: config.space.S200 }}
|
||||
{/* Media area with nav arrows */}
|
||||
<Box
|
||||
grow="Yes"
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
style={{ overflow: 'hidden', padding: config.space.S400 }}
|
||||
>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
<Box
|
||||
grow="Yes"
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
style={{ overflow: 'hidden', height: '100%' }}
|
||||
>
|
||||
<LightboxMedia
|
||||
key={`${item.mxcUrl}-${item.ts}`}
|
||||
item={item}
|
||||
useAuthentication={useAuthentication}
|
||||
/>
|
||||
</Box>
|
||||
{index < items.length - 1 && (
|
||||
<IconButton
|
||||
variant="Surface"
|
||||
aria-label="Next"
|
||||
onClick={next}
|
||||
style={{ flexShrink: 0, marginLeft: config.space.S200 }}
|
||||
>
|
||||
<Icon src={Icons.ArrowRight} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
{index > 0 && (
|
||||
<IconButton
|
||||
variant="Surface"
|
||||
aria-label="Previous"
|
||||
onClick={prev}
|
||||
style={{ flexShrink: 0, marginRight: config.space.S200 }}
|
||||
>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
<Box
|
||||
grow="Yes"
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
style={{ overflow: 'hidden', height: '100%' }}
|
||||
>
|
||||
<LightboxMedia
|
||||
key={`${item.mxcUrl}-${item.ts}`}
|
||||
item={item}
|
||||
useAuthentication={useAuthentication}
|
||||
/>
|
||||
</Box>
|
||||
{index < items.length - 1 && (
|
||||
<IconButton
|
||||
variant="Surface"
|
||||
aria-label="Next"
|
||||
onClick={next}
|
||||
style={{ flexShrink: 0, marginLeft: config.space.S200 }}
|
||||
>
|
||||
<Icon src={Icons.ArrowRight} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
</FocusTrap>
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user