mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 11:22:31 +00:00
fix(accessibility) improve file actions with focus management and ARIA roles (#16322)
This commit is contained in:
@@ -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<HTMLInputElement>(null);
|
||||
const uploadButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const { t } = useTranslation();
|
||||
@@ -264,6 +301,7 @@ const FileSharing = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
{
|
||||
@@ -312,8 +350,12 @@ const FileSharing = () => {
|
||||
{
|
||||
sortedFiles.map(file => (
|
||||
<li
|
||||
className = { classes.fileItem }
|
||||
className = { `${classes.fileItem} ${isFocused ? 'focused' : ''}` }
|
||||
key = { file.fileId }
|
||||
// Only remove focus when leaving the whole fileItem, not just moving between its buttons
|
||||
onBlur = { e => !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) }
|
||||
</pre>
|
||||
</div>
|
||||
<div className = { classes.buttonContainer }>
|
||||
<Icon
|
||||
className = { `${classes.actionIcon} actionIconVisibility` }
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<div className = { `${classes.buttonContainer} actionIconVisibility` }>
|
||||
<button
|
||||
aria-label = { `${t('fileSharing.downloadFile')} ${file.fileName}` }
|
||||
className = { `${classes.iconButton}` }
|
||||
onClick = { () => dispatch(downloadFile(file.fileId)) }
|
||||
size = { 24 }
|
||||
src = { IconDownload } />
|
||||
type = 'button'>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
size = { 24 }
|
||||
src = { IconDownload } />
|
||||
</button>
|
||||
|
||||
{
|
||||
isUploadEnabled && (
|
||||
<Icon
|
||||
className = { `${classes.actionIcon} actionIconVisibility` }
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<button
|
||||
aria-label = { `${t('fileSharing.removeFile')} ${file.fileName}` }
|
||||
className = { `${classes.iconButton}` }
|
||||
onClick = { () => dispatch(removeFile(file.fileId)) }
|
||||
size = { 24 }
|
||||
src = { IconTrash } />
|
||||
type = 'button'>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
size = { 24 }
|
||||
src = { IconTrash } />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user