From 32df2842777bf6a4a75bb8d8825015174aaafd2f Mon Sep 17 00:00:00 2001 From: Mihaela Dumitru Date: Tue, 26 Aug 2025 16:29:05 +0300 Subject: [PATCH] fix(accessibility) improve file actions with focus management and ARIA roles (#16322) --- lang/main.json | 2 + .../components/web/FileSharing.tsx | 93 ++++++++++++++----- react/features/file-sharing/middleware.web.ts | 9 +- 3 files changed, 80 insertions(+), 24 deletions(-) diff --git a/lang/main.json b/lang/main.json index e4346b75f7..0e234378b7 100644 --- a/lang/main.json +++ b/lang/main.json @@ -566,6 +566,7 @@ "downloadFailedDescription": "Please try again.", "downloadFailedTitle": "Download failed", "downloadFile": "Download", + "downloadStarted": "File download started", "dragAndDrop": "Drag and drop files here or anywhere on screen", "fileAlreadyUploaded": "File has already been uploaded to this meeting.", "fileTooLargeDescription": "Please make sure the file does not exceed {{ maxFileSize }}.", @@ -573,6 +574,7 @@ "fileUploadProgress": "File upload progress", "fileUploadedSuccessfully": "File uploaded successfully", "removeFile": "Remove", + "removeFileSuccess": "File removed successfully", "uploadFailedDescription": "Please try again.", "uploadFailedTitle": "Upload failed", "uploadFile": "Share file" diff --git a/react/features/file-sharing/components/web/FileSharing.tsx b/react/features/file-sharing/components/web/FileSharing.tsx index 503aa6d93a..68c5299e15 100644 --- a/react/features/file-sharing/components/web/FileSharing.tsx +++ b/react/features/file-sharing/components/web/FileSharing.tsx @@ -23,14 +23,13 @@ const useStyles = makeStyles()(theme => { return { buttonContainer: { alignItems: 'center', + bottom: 0, display: 'flex', justifyContent: 'end', gap: theme.spacing(2), position: 'absolute', - top: 0, right: theme.spacing(3), - bottom: 0, - left: 0 + top: 0 }, container: { @@ -80,17 +79,33 @@ const useStyles = makeStyles()(theme => { padding: theme.spacing(3), position: 'relative', + '& .actionIconVisibility': { + opacity: 0, + transition: 'opacity 0.2s' + }, + + '& .timestampVisibility': { + opacity: 1 + }, + '&:hover': { backgroundColor: theme.palette.ui03, - borderRadius: theme.shape.borderRadius, '& .actionIconVisibility': { - visibility: 'visible' + opacity: 1 }, '& .timestampVisibility': { - visibility: 'hidden' + opacity: 0 } + }, + + '&.focused .actionIconVisibility': { + opacity: 1 + }, + + '&.focused .timestampVisibility': { + opacity: 0 } }, @@ -198,9 +213,30 @@ const useStyles = makeStyles()(theme => { }, actionIcon: { + background: 'transparent', + border: 0, cursor: 'pointer', padding: theme.spacing(1), - visibility: 'hidden' + visibility: 'hidden', + '&:focus': { + outline: `2px solid ${theme.palette.action01}` + } + }, + + iconButton: { + background: 'none', + border: 'none', + padding: 0, + marginLeft: '8px', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + + '&:focus-visible': { + outline: `2px solid ${theme.palette.action01}`, + borderRadius: '4px' + } } }; }); @@ -208,6 +244,7 @@ const useStyles = makeStyles()(theme => { const FileSharing = () => { const { classes } = useStyles(); const [ isDragging, setIsDragging ] = useState(false); + const [ isFocused, setIsFocused ] = useState(false); const fileInputRef = useRef(null); const uploadButtonRef = useRef(null); const { t } = useTranslation(); @@ -264,6 +301,7 @@ const FileSharing = () => { } }, []); + /* eslint-disable react/jsx-no-bind */ return (
{ @@ -312,8 +350,12 @@ const FileSharing = () => { { sortedFiles.map(file => (
  • !e.currentTarget.contains(e.relatedTarget as Node) && setIsFocused(false) } + onFocus = { () => setIsFocused(true) } + tabIndex = { -1 } title = { file.fileName }> { (file.progress ?? 100) === 100 && ( @@ -346,25 +388,30 @@ const FileSharing = () => { { formatTimestamp(file.timestamp) }
  • -
    - + + { isUploadEnabled && ( - dispatch(removeFile(file.fileId)) } - size = { 24 } - src = { IconTrash } /> + type = 'button'> + + ) }
    diff --git a/react/features/file-sharing/middleware.web.ts b/react/features/file-sharing/middleware.web.ts index 25ab147571..f9754d0b7a 100644 --- a/react/features/file-sharing/middleware.web.ts +++ b/react/features/file-sharing/middleware.web.ts @@ -6,7 +6,7 @@ import { JitsiConferenceEvents } from '../base/lib-jitsi-meet'; import { getLocalParticipant, getParticipantDisplayName } from '../base/participants/functions'; import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; import StateListenerRegistry from '../base/redux/StateListenerRegistry'; -import { showErrorNotification, showSuccessNotification } from '../notifications/actions'; +import { showErrorNotification, showNotification, showSuccessNotification } from '../notifications/actions'; import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants'; import { DOWNLOAD_FILE, REMOVE_FILE, UPLOAD_FILES, _FILE_LIST_RECEIVED, _FILE_REMOVED } from './actionTypes'; @@ -101,6 +101,9 @@ MiddlewareRegistry.register(store => next => action => { if (!response.ok) { throw new Error(`Failed to delete file: ${response.statusText}`); } + store.dispatch(showSuccessNotification({ + titleKey: 'fileSharing.removeFileSuccess' + }, NOTIFICATION_TIMEOUT_TYPE.SHORT)); }) .catch((error: any) => { logger.warn('Could not delete file:', error); @@ -130,6 +133,10 @@ MiddlewareRegistry.register(store => next => action => { throw new Error('No presigned URL found in the response.'); } + store.dispatch(showNotification({ + titleKey: 'fileSharing.downloadStarted' + }, NOTIFICATION_TIMEOUT_TYPE.SHORT)); + return downloadFile(presignedUrl, fileName); }) .catch((error: any) => {