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,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
Icons,
|
Icons,
|
||||||
|
Overlay,
|
||||||
|
OverlayBackdrop,
|
||||||
Scroll,
|
Scroll,
|
||||||
Spinner,
|
Spinner,
|
||||||
Text,
|
Text,
|
||||||
@@ -15,6 +17,7 @@ import {
|
|||||||
config,
|
config,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { EventType, MatrixClient, MatrixEvent, MsgType, Room } from 'matrix-js-sdk';
|
import { EventType, MatrixClient, MatrixEvent, MsgType, Room } from 'matrix-js-sdk';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useNearViewport } from '../../hooks/useNearViewport';
|
import { useNearViewport } from '../../hooks/useNearViewport';
|
||||||
import { IEncryptedFile, IImageInfo, IThumbnailContent } from '../../../types/matrix/common';
|
import { IEncryptedFile, IImageInfo, IThumbnailContent } from '../../../types/matrix/common';
|
||||||
@@ -250,102 +253,112 @@ function Lightbox({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||||
<div
|
<FocusTrap
|
||||||
role="dialog"
|
focusTrapOptions={{
|
||||||
aria-modal
|
initialFocus: false,
|
||||||
aria-label="Media viewer"
|
clickOutsideDeactivates: false,
|
||||||
onKeyDown={handleKeyDown}
|
escapeDeactivates: false,
|
||||||
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,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box grow="Yes" direction="Column" style={{ overflow: 'hidden' }}>
|
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||||
<Text size="T400" truncate style={{ color: '#fff', fontWeight: 500 }}>
|
<div
|
||||||
{item.body || (item.msgtype === MsgType.Video ? 'Video' : 'Image')}
|
role="dialog"
|
||||||
</Text>
|
aria-modal
|
||||||
<Text size="T200" style={{ color: 'rgba(255,255,255,0.5)' }}>
|
aria-label="Media viewer"
|
||||||
{item.sender} · {dateStr}
|
onKeyDown={handleKeyDown}
|
||||||
</Text>
|
tabIndex={-1}
|
||||||
</Box>
|
style={{
|
||||||
<Text size="T200" style={{ color: 'rgba(255,255,255,0.4)', flexShrink: 0 }}>
|
position: 'fixed',
|
||||||
{index + 1} / {items.length}
|
inset: 0,
|
||||||
</Text>
|
zIndex: 1000,
|
||||||
<TooltipProvider
|
background: 'rgba(0,0,0,0.92)',
|
||||||
position="Bottom"
|
display: 'flex',
|
||||||
align="End"
|
flexDirection: 'column',
|
||||||
offset={4}
|
}}
|
||||||
tooltip={
|
|
||||||
<Tooltip>
|
|
||||||
<Text>Close</Text>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{(ref) => (
|
{/* Header bar */}
|
||||||
<IconButton ref={ref} variant="Surface" aria-label="Close" onClick={onClose}>
|
<Box
|
||||||
<Icon src={Icons.Cross} />
|
alignItems="Center"
|
||||||
</IconButton>
|
gap="200"
|
||||||
)}
|
style={{
|
||||||
</TooltipProvider>
|
padding: `${config.space.S200} ${config.space.S300}`,
|
||||||
</Box>
|
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 */}
|
{/* Media area with nav arrows */}
|
||||||
<Box
|
<Box
|
||||||
grow="Yes"
|
grow="Yes"
|
||||||
alignItems="Center"
|
alignItems="Center"
|
||||||
justifyContent="Center"
|
justifyContent="Center"
|
||||||
style={{ overflow: 'hidden', padding: config.space.S400 }}
|
style={{ overflow: 'hidden', padding: config.space.S400 }}
|
||||||
>
|
|
||||||
{index > 0 && (
|
|
||||||
<IconButton
|
|
||||||
variant="Surface"
|
|
||||||
aria-label="Previous"
|
|
||||||
onClick={prev}
|
|
||||||
style={{ flexShrink: 0, marginRight: config.space.S200 }}
|
|
||||||
>
|
>
|
||||||
<Icon src={Icons.ArrowLeft} />
|
{index > 0 && (
|
||||||
</IconButton>
|
<IconButton
|
||||||
)}
|
variant="Surface"
|
||||||
<Box
|
aria-label="Previous"
|
||||||
grow="Yes"
|
onClick={prev}
|
||||||
alignItems="Center"
|
style={{ flexShrink: 0, marginRight: config.space.S200 }}
|
||||||
justifyContent="Center"
|
>
|
||||||
style={{ overflow: 'hidden', height: '100%' }}
|
<Icon src={Icons.ArrowLeft} />
|
||||||
>
|
</IconButton>
|
||||||
<LightboxMedia
|
)}
|
||||||
key={`${item.mxcUrl}-${item.ts}`}
|
<Box
|
||||||
item={item}
|
grow="Yes"
|
||||||
useAuthentication={useAuthentication}
|
alignItems="Center"
|
||||||
/>
|
justifyContent="Center"
|
||||||
</Box>
|
style={{ overflow: 'hidden', height: '100%' }}
|
||||||
{index < items.length - 1 && (
|
>
|
||||||
<IconButton
|
<LightboxMedia
|
||||||
variant="Surface"
|
key={`${item.mxcUrl}-${item.ts}`}
|
||||||
aria-label="Next"
|
item={item}
|
||||||
onClick={next}
|
useAuthentication={useAuthentication}
|
||||||
style={{ flexShrink: 0, marginLeft: config.space.S200 }}
|
/>
|
||||||
>
|
</Box>
|
||||||
<Icon src={Icons.ArrowRight} />
|
{index < items.length - 1 && (
|
||||||
</IconButton>
|
<IconButton
|
||||||
)}
|
variant="Surface"
|
||||||
</Box>
|
aria-label="Next"
|
||||||
</div>
|
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