WIP: Accessibility of web app
This commit is contained in:
parent
4a8678bf39
commit
bb5e0e3fed
9 changed files with 170 additions and 77 deletions
|
@ -1,10 +1,16 @@
|
||||||
{
|
{
|
||||||
|
"action_bar_show_menu": "Show menu",
|
||||||
|
"action_bar_logo_alt": "ntfy logo",
|
||||||
"action_bar_settings": "Settings",
|
"action_bar_settings": "Settings",
|
||||||
"action_bar_send_test_notification": "Send test notification",
|
"action_bar_send_test_notification": "Send test notification",
|
||||||
"action_bar_clear_notifications": "Clear all notifications",
|
"action_bar_clear_notifications": "Clear all notifications",
|
||||||
"action_bar_unsubscribe": "Unsubscribe",
|
"action_bar_unsubscribe": "Unsubscribe",
|
||||||
|
"action_bar_toggle_mute": "Toggle mute notifications",
|
||||||
|
"action_bar_toggle_action_menu": "Toggle action menu",
|
||||||
"message_bar_type_message": "Type a message here",
|
"message_bar_type_message": "Type a message here",
|
||||||
"message_bar_error_publishing": "Error publishing notification",
|
"message_bar_error_publishing": "Error publishing notification",
|
||||||
|
"message_bar_show_dialog": "Show publish dialog",
|
||||||
|
"message_bar_publish": "Publish message",
|
||||||
"nav_topics_title": "Subscribed topics",
|
"nav_topics_title": "Subscribed topics",
|
||||||
"nav_button_all_notifications": "All notifications",
|
"nav_button_all_notifications": "All notifications",
|
||||||
"nav_button_settings": "Settings",
|
"nav_button_settings": "Settings",
|
||||||
|
@ -16,14 +22,25 @@
|
||||||
"alert_grant_button": "Grant now",
|
"alert_grant_button": "Grant now",
|
||||||
"alert_not_supported_title": "Notifications not supported",
|
"alert_not_supported_title": "Notifications not supported",
|
||||||
"alert_not_supported_description": "Notifications are not supported in your browser.",
|
"alert_not_supported_description": "Notifications are not supported in your browser.",
|
||||||
|
"notifications_list": "Notifications list",
|
||||||
|
"notifications_list_item": "Notification",
|
||||||
|
"notifications_delete": "Delete notification",
|
||||||
"notifications_copied_to_clipboard": "Copied to clipboard",
|
"notifications_copied_to_clipboard": "Copied to clipboard",
|
||||||
"notifications_tags": "Tags",
|
"notifications_tags": "Tags",
|
||||||
|
"notifications_priority_x": "Priority {{priority}}",
|
||||||
|
"notifications_new_indicator": "New notification",
|
||||||
|
"notifications_attachment_image": "Attachment image",
|
||||||
"notifications_attachment_copy_url_title": "Copy attachment URL to clipboard",
|
"notifications_attachment_copy_url_title": "Copy attachment URL to clipboard",
|
||||||
"notifications_attachment_copy_url_button": "Copy URL",
|
"notifications_attachment_copy_url_button": "Copy URL",
|
||||||
"notifications_attachment_open_title": "Go to {{url}}",
|
"notifications_attachment_open_title": "Go to {{url}}",
|
||||||
"notifications_attachment_open_button": "Open attachment",
|
"notifications_attachment_open_button": "Open attachment",
|
||||||
"notifications_attachment_link_expires": "link expires {{date}}",
|
"notifications_attachment_link_expires": "link expires {{date}}",
|
||||||
"notifications_attachment_link_expired": "download link expired",
|
"notifications_attachment_link_expired": "download link expired",
|
||||||
|
"notifications_attachment_file_image": "image file",
|
||||||
|
"notifications_attachment_file_video": "video file",
|
||||||
|
"notifications_attachment_file_audio": "audio file",
|
||||||
|
"notifications_attachment_file_app": "Android app file",
|
||||||
|
"notifications_attachment_file_document": "other document",
|
||||||
"notifications_click_copy_url_title": "Copy link URL to clipboard",
|
"notifications_click_copy_url_title": "Copy link URL to clipboard",
|
||||||
"notifications_click_copy_url_button": "Copy link",
|
"notifications_click_copy_url_button": "Copy link",
|
||||||
"notifications_click_open_button": "Open link",
|
"notifications_click_open_button": "Open link",
|
||||||
|
@ -47,6 +64,7 @@
|
||||||
"publish_dialog_attachment_limits_file_and_quota_reached": "exceeds {{fileSizeLimit}} file limit and quota, {{remainingBytes}} remaining",
|
"publish_dialog_attachment_limits_file_and_quota_reached": "exceeds {{fileSizeLimit}} file limit and quota, {{remainingBytes}} remaining",
|
||||||
"publish_dialog_attachment_limits_file_reached": "exceeds {{fileSizeLimit}} file limit",
|
"publish_dialog_attachment_limits_file_reached": "exceeds {{fileSizeLimit}} file limit",
|
||||||
"publish_dialog_attachment_limits_quota_reached": "exceeds quota, {{remainingBytes}} remaining",
|
"publish_dialog_attachment_limits_quota_reached": "exceeds quota, {{remainingBytes}} remaining",
|
||||||
|
"publish_dialog_emoji_picker_show": "Pick emoji",
|
||||||
"publish_dialog_priority_min": "Min. priority",
|
"publish_dialog_priority_min": "Min. priority",
|
||||||
"publish_dialog_priority_low": "Low priority",
|
"publish_dialog_priority_low": "Low priority",
|
||||||
"publish_dialog_priority_default": "Default priority",
|
"publish_dialog_priority_default": "Default priority",
|
||||||
|
@ -89,6 +107,7 @@
|
||||||
"publish_dialog_attached_file_filename_placeholder": "Attachment filename",
|
"publish_dialog_attached_file_filename_placeholder": "Attachment filename",
|
||||||
"publish_dialog_drop_file_here": "Drop file here",
|
"publish_dialog_drop_file_here": "Drop file here",
|
||||||
"emoji_picker_search_placeholder": "Search emoji",
|
"emoji_picker_search_placeholder": "Search emoji",
|
||||||
|
"emoji_picker_search_clear": "Clear search",
|
||||||
"subscribe_dialog_subscribe_title": "Subscribe to topic",
|
"subscribe_dialog_subscribe_title": "Subscribe to topic",
|
||||||
"subscribe_dialog_subscribe_description": "Topics may not be password-protected, so choose a name that's not easy to guess. Once subscribed, you can PUT/POST notifications.",
|
"subscribe_dialog_subscribe_description": "Topics may not be password-protected, so choose a name that's not easy to guess. Once subscribed, you can PUT/POST notifications.",
|
||||||
"subscribe_dialog_subscribe_topic_placeholder": "Topic name, e.g. phil_alerts",
|
"subscribe_dialog_subscribe_topic_placeholder": "Topic name, e.g. phil_alerts",
|
||||||
|
@ -108,6 +127,7 @@
|
||||||
"prefs_notifications_sound_description_none": "Notifications do not play any sound when they arrive",
|
"prefs_notifications_sound_description_none": "Notifications do not play any sound when they arrive",
|
||||||
"prefs_notifications_sound_description_some": "Notifications play the {{sound}} sound when they arrive",
|
"prefs_notifications_sound_description_some": "Notifications play the {{sound}} sound when they arrive",
|
||||||
"prefs_notifications_sound_no_sound": "No sound",
|
"prefs_notifications_sound_no_sound": "No sound",
|
||||||
|
"prefs_notifications_sound_play": "Play selected sound",
|
||||||
"prefs_notifications_min_priority_title": "Minimum priority",
|
"prefs_notifications_min_priority_title": "Minimum priority",
|
||||||
"prefs_notifications_min_priority_description_any": "Showing all notifications, regardless of priority",
|
"prefs_notifications_min_priority_description_any": "Showing all notifications, regardless of priority",
|
||||||
"prefs_notifications_min_priority_description_x_or_higher": "Show notifications if priority is {{number}} ({{name}}) or above",
|
"prefs_notifications_min_priority_description_x_or_higher": "Show notifications if priority is {{number}} ({{name}}) or above",
|
||||||
|
@ -130,7 +150,10 @@
|
||||||
"prefs_notifications_delete_after_one_month_description": "Notifications are auto-deleted after one month",
|
"prefs_notifications_delete_after_one_month_description": "Notifications are auto-deleted after one month",
|
||||||
"prefs_users_title": "Manage users",
|
"prefs_users_title": "Manage users",
|
||||||
"prefs_users_description": "Add/remove users for your protected topics here. Please note that username and password are stored in the browser's local storage.",
|
"prefs_users_description": "Add/remove users for your protected topics here. Please note that username and password are stored in the browser's local storage.",
|
||||||
|
"prefs_users_table": "Users table",
|
||||||
"prefs_users_add_button": "Add user",
|
"prefs_users_add_button": "Add user",
|
||||||
|
"prefs_users_edit_button": "Edit user",
|
||||||
|
"prefs_users_delete_button": "Delete user",
|
||||||
"prefs_users_table_user_header": "User",
|
"prefs_users_table_user_header": "User",
|
||||||
"prefs_users_table_base_url_header": "Service URL",
|
"prefs_users_table_base_url_header": "Service URL",
|
||||||
"prefs_users_dialog_title_add": "Add user",
|
"prefs_users_dialog_title_add": "Add user",
|
||||||
|
|
|
@ -44,16 +44,22 @@ const ActionBar = (props) => {
|
||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
edge="start"
|
edge="start"
|
||||||
|
aria-label={t("action_bar_show_menu")}
|
||||||
onClick={props.onMobileDrawerToggle}
|
onClick={props.onMobileDrawerToggle}
|
||||||
sx={{ mr: 2, display: { sm: 'none' } }}
|
sx={{ mr: 2, display: { sm: 'none' } }}
|
||||||
>
|
>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Box component="img" src={logo} sx={{
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={logo}
|
||||||
|
alt={t("action_bar_logo_alt")}
|
||||||
|
sx={{
|
||||||
display: { xs: 'none', sm: 'block' },
|
display: { xs: 'none', sm: 'block' },
|
||||||
marginRight: '10px',
|
marginRight: '10px',
|
||||||
height: '28px'
|
height: '28px'
|
||||||
}}/>
|
}}
|
||||||
|
/>
|
||||||
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@ -173,10 +179,10 @@ const SettingsIcons = (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton color="inherit" size="large" edge="end" onClick={handleToggleMute} sx={{marginRight: 0}}>
|
<IconButton color="inherit" size="large" edge="end" onClick={handleToggleMute} sx={{marginRight: 0}} aria-label={t("action_bar_toggle_mute")}>
|
||||||
{subscription.mutedUntil ? <NotificationsOffIcon/> : <NotificationsIcon/>}
|
{subscription.mutedUntil ? <NotificationsOffIcon/> : <NotificationsIcon/>}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton color="inherit" size="large" edge="end" ref={anchorRef} onClick={handleToggleOpen}>
|
<IconButton color="inherit" size="large" edge="end" ref={anchorRef} onClick={handleToggleOpen} aria-label={t("action_bar_toggle_action_menu")}>
|
||||||
<MoreVertIcon/>
|
<MoreVertIcon/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Popper
|
<Popper
|
||||||
|
|
|
@ -5,27 +5,36 @@ import fileImage from "../img/file-image.svg";
|
||||||
import fileVideo from "../img/file-video.svg";
|
import fileVideo from "../img/file-video.svg";
|
||||||
import fileAudio from "../img/file-audio.svg";
|
import fileAudio from "../img/file-audio.svg";
|
||||||
import fileApp from "../img/file-app.svg";
|
import fileApp from "../img/file-app.svg";
|
||||||
|
import {useTranslation} from "react-i18next";
|
||||||
|
|
||||||
const AttachmentIcon = (props) => {
|
const AttachmentIcon = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const type = props.type;
|
const type = props.type;
|
||||||
let imageFile;
|
let imageFile, imageLabel;
|
||||||
if (!type) {
|
if (!type) {
|
||||||
imageFile = fileDocument;
|
imageFile = fileDocument;
|
||||||
|
imageLabel = t("notifications_attachment_file_image");
|
||||||
} else if (type.startsWith('image/')) {
|
} else if (type.startsWith('image/')) {
|
||||||
imageFile = fileImage;
|
imageFile = fileImage;
|
||||||
|
imageLabel = t("notifications_attachment_file_video");
|
||||||
} else if (type.startsWith('video/')) {
|
} else if (type.startsWith('video/')) {
|
||||||
imageFile = fileVideo;
|
imageFile = fileVideo;
|
||||||
|
imageLabel = t("notifications_attachment_file_video");
|
||||||
} else if (type.startsWith('audio/')) {
|
} else if (type.startsWith('audio/')) {
|
||||||
imageFile = fileAudio;
|
imageFile = fileAudio;
|
||||||
|
imageLabel = t("notifications_attachment_file_audio");
|
||||||
} else if (type === "application/vnd.android.package-archive") {
|
} else if (type === "application/vnd.android.package-archive") {
|
||||||
imageFile = fileApp;
|
imageFile = fileApp;
|
||||||
|
imageLabel = t("notifications_attachment_file_app");
|
||||||
} else {
|
} else {
|
||||||
imageFile = fileDocument;
|
imageFile = fileDocument;
|
||||||
|
imageLabel = t("notifications_attachment_file_document");
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
component="img"
|
component="img"
|
||||||
src={imageFile}
|
src={imageFile}
|
||||||
|
alt={imageLabel}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
sx={{
|
sx={{
|
||||||
width: '28px',
|
width: '28px',
|
||||||
|
|
|
@ -73,6 +73,8 @@ const EmojiPicker = (props) => {
|
||||||
inputRef={searchRef}
|
inputRef={searchRef}
|
||||||
margin="dense"
|
margin="dense"
|
||||||
size="small"
|
size="small"
|
||||||
|
role="searchbox"
|
||||||
|
aria-label={t("emoji_picker_search_placeholder")}
|
||||||
placeholder={t("emoji_picker_search_placeholder")}
|
placeholder={t("emoji_picker_search_placeholder")}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={ev => setSearch(ev.target.value)}
|
onChange={ev => setSearch(ev.target.value)}
|
||||||
|
@ -83,7 +85,9 @@ const EmojiPicker = (props) => {
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment:
|
endAdornment:
|
||||||
<InputAdornment position="end" sx={{ display: (search) ? '' : 'none' }}>
|
<InputAdornment position="end" sx={{ display: (search) ? '' : 'none' }}>
|
||||||
<IconButton size="small" onClick={handleSearchClear} edge="end"><Close/></IconButton>
|
<IconButton size="small" onClick={handleSearchClear} edge="end" aria-label={t("emoji_picker_search_clear")}>
|
||||||
|
<Close/>
|
||||||
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -130,10 +134,12 @@ const Category = (props) => {
|
||||||
const Emoji = (props) => {
|
const Emoji = (props) => {
|
||||||
const emoji = props.emoji;
|
const emoji = props.emoji;
|
||||||
const matches = emojiMatches(emoji, props.search);
|
const matches = emojiMatches(emoji, props.search);
|
||||||
|
const title = `${emoji.description} (${emoji.aliases[0]})`;
|
||||||
return (
|
return (
|
||||||
<EmojiDiv
|
<EmojiDiv
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
title={`${emoji.description} (${emoji.aliases[0]})`}
|
title={title}
|
||||||
|
aria-label={title}
|
||||||
style={{ display: (matches) ? '' : 'none' }}
|
style={{ display: (matches) ? '' : 'none' }}
|
||||||
>
|
>
|
||||||
{props.emoji.emoji}
|
{props.emoji.emoji}
|
||||||
|
|
|
@ -75,13 +75,15 @@ const MessageBar = (props) => {
|
||||||
backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900]
|
backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900]
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton color="inherit" size="large" edge="start" onClick={props.onOpenDialogClick}>
|
<IconButton color="inherit" size="large" edge="start" onClick={props.onOpenDialogClick} aria-label={t("message_bar_show_dialog")}>
|
||||||
<KeyboardArrowUpIcon/>
|
<KeyboardArrowUpIcon/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
margin="dense"
|
margin="dense"
|
||||||
placeholder={t("message_bar_type_message")}
|
placeholder={t("message_bar_type_message")}
|
||||||
|
aria-label={t("message_bar_type_message")}
|
||||||
|
role="textbox"
|
||||||
type="text"
|
type="text"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="standard"
|
variant="standard"
|
||||||
|
@ -94,7 +96,7 @@ const MessageBar = (props) => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton color="inherit" size="large" edge="end" onClick={handleSendClick}>
|
<IconButton color="inherit" size="large" edge="end" onClick={handleSendClick} aria-label={t("message_bar_publish")}>
|
||||||
<SendIcon/>
|
<SendIcon/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Portal>
|
<Portal>
|
||||||
|
|
|
@ -31,10 +31,15 @@ const navWidth = 280;
|
||||||
const Navigation = (props) => {
|
const Navigation = (props) => {
|
||||||
const navigationList = <NavList {...props}/>;
|
const navigationList = <NavList {...props}/>;
|
||||||
return (
|
return (
|
||||||
<Box component="nav" sx={{width: {sm: Navigation.width}, flexShrink: {sm: 0}}}>
|
<Box
|
||||||
|
component="nav"
|
||||||
|
role="navigation"
|
||||||
|
sx={{width: {sm: Navigation.width}, flexShrink: {sm: 0}}}
|
||||||
|
>
|
||||||
{/* Mobile drawer; only shown if menu icon clicked (mobile open) and display is small */}
|
{/* Mobile drawer; only shown if menu icon clicked (mobile open) and display is small */}
|
||||||
<Drawer
|
<Drawer
|
||||||
variant="temporary"
|
variant="temporary"
|
||||||
|
role="menubar"
|
||||||
open={props.mobileDrawerOpen}
|
open={props.mobileDrawerOpen}
|
||||||
onClose={props.onMobileDrawerToggle}
|
onClose={props.onMobileDrawerToggle}
|
||||||
ModalProps={{ keepMounted: true }} // Better open performance on mobile.
|
ModalProps={{ keepMounted: true }} // Better open performance on mobile.
|
||||||
|
@ -49,6 +54,7 @@ const Navigation = (props) => {
|
||||||
<Drawer
|
<Drawer
|
||||||
open
|
open
|
||||||
variant="permanent"
|
variant="permanent"
|
||||||
|
role="menubar"
|
||||||
sx={{
|
sx={{
|
||||||
display: { xs: 'none', sm: 'block' },
|
display: { xs: 'none', sm: 'block' },
|
||||||
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: navWidth },
|
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: navWidth },
|
||||||
|
|
|
@ -98,6 +98,8 @@ const NotificationList = (props) => {
|
||||||
>
|
>
|
||||||
<Container
|
<Container
|
||||||
maxWidth="md"
|
maxWidth="md"
|
||||||
|
role="list"
|
||||||
|
aria-label={t("notifications_list")}
|
||||||
sx={{
|
sx={{
|
||||||
marginTop: 3,
|
marginTop: 3,
|
||||||
marginBottom: (props.messageBar) ? "100px" : 3 // Hack to avoid hiding notifications behind the message bar
|
marginBottom: (props.messageBar) ? "100px" : 3 // Hack to avoid hiding notifications behind the message bar
|
||||||
|
@ -143,9 +145,9 @@ const NotificationItem = (props) => {
|
||||||
const hasUserActions = notification.actions && notification.actions.length > 0;
|
const hasUserActions = notification.actions && notification.actions.length > 0;
|
||||||
const showActions = hasAttachmentActions || hasClickAction || hasUserActions;
|
const showActions = hasAttachmentActions || hasClickAction || hasUserActions;
|
||||||
return (
|
return (
|
||||||
<Card sx={{ minWidth: 275, padding: 1 }}>
|
<Card sx={{ minWidth: 275, padding: 1 }} role="listitem" aria-label={t("notifications_list_item")}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<IconButton onClick={handleDelete} sx={{ float: 'right', marginRight: -1, marginTop: -1 }}>
|
<IconButton onClick={handleDelete} sx={{ float: 'right', marginRight: -1, marginTop: -1 }} aria-label={t("notifications_delete")}>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography sx={{ fontSize: 14 }} color="text.secondary">
|
<Typography sx={{ fontSize: 14 }} color="text.secondary">
|
||||||
|
@ -153,15 +155,15 @@ const NotificationItem = (props) => {
|
||||||
{[1,2,4,5].includes(notification.priority) &&
|
{[1,2,4,5].includes(notification.priority) &&
|
||||||
<img
|
<img
|
||||||
src={priorityFiles[notification.priority]}
|
src={priorityFiles[notification.priority]}
|
||||||
alt={`Priority ${notification.priority}`}
|
alt={t("notifications_priority_x", { priority: notification.priority})}
|
||||||
style={{ verticalAlign: 'bottom' }}
|
style={{ verticalAlign: 'bottom' }}
|
||||||
/>}
|
/>}
|
||||||
{notification.new === 1 &&
|
{notification.new === 1 &&
|
||||||
<svg style={{ width: '8px', height: '8px', marginLeft: '4px' }} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
<svg style={{ width: '8px', height: '8px', marginLeft: '4px' }} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" aria-label={t("notifications_new_indicator")}>
|
||||||
<circle cx="50" cy="50" r="50" fill="#338574"/>
|
<circle cx="50" cy="50" r="50" fill="#338574"/>
|
||||||
</svg>}
|
</svg>}
|
||||||
</Typography>
|
</Typography>
|
||||||
{notification.title && <Typography variant="h5" component="div">{formatTitle(notification)}</Typography>}
|
{notification.title && <Typography variant="h5" component="div" role="rowheader">{formatTitle(notification)}</Typography>}
|
||||||
<Typography variant="body1" sx={{ whiteSpace: 'pre-line' }}>
|
<Typography variant="body1" sx={{ whiteSpace: 'pre-line' }}>
|
||||||
{autolink(maybeAppendActionErrors(formatMessage(notification), notification))}
|
{autolink(maybeAppendActionErrors(formatMessage(notification), notification))}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@ -289,6 +291,7 @@ const Attachment = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Image = (props) => {
|
const Image = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -296,6 +299,7 @@ const Image = (props) => {
|
||||||
component="img"
|
component="img"
|
||||||
src={props.attachment.url}
|
src={props.attachment.url}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
alt={t("notifications_attachment_image")}
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
sx={{
|
sx={{
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
|
@ -316,6 +320,7 @@ const Image = (props) => {
|
||||||
<Box
|
<Box
|
||||||
component="img"
|
component="img"
|
||||||
src={props.attachment.url}
|
src={props.attachment.url}
|
||||||
|
alt={t("notifications_attachment_image")}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
sx={{
|
sx={{
|
||||||
maxWidth: 1,
|
maxWidth: 1,
|
||||||
|
@ -347,13 +352,16 @@ const UserAction = (props) => {
|
||||||
if (action.action === "broadcast") {
|
if (action.action === "broadcast") {
|
||||||
return (
|
return (
|
||||||
<Tooltip title={t("notifications_actions_not_supported")}>
|
<Tooltip title={t("notifications_actions_not_supported")}>
|
||||||
<span><Button disabled>{action.label}</Button></span>
|
<span><Button disabled aria-label={t("notifications_actions_not_supported")}>{action.label}</Button></span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
} else if (action.action === "view") {
|
} else if (action.action === "view") {
|
||||||
return (
|
return (
|
||||||
<Tooltip title={t("notifications_actions_open_url_title", { url: action.url })}>
|
<Tooltip title={t("notifications_actions_open_url_title", { url: action.url })}>
|
||||||
<Button onClick={() => openUrl(action.url)}>{action.label}</Button>
|
<Button
|
||||||
|
onClick={() => openUrl(action.url)}
|
||||||
|
aria-label={t("notifications_actions_open_url_title", { url: action.url })}
|
||||||
|
>{action.label}</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
} else if (action.action === "http") {
|
} else if (action.action === "http") {
|
||||||
|
@ -361,7 +369,10 @@ const UserAction = (props) => {
|
||||||
const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? "");
|
const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? "");
|
||||||
return (
|
return (
|
||||||
<Tooltip title={t("notifications_actions_http_request_title", { method: method, url: action.url })}>
|
<Tooltip title={t("notifications_actions_http_request_title", { method: method, url: action.url })}>
|
||||||
<Button onClick={() => performHttpAction(notification, action)}>{label}</Button>
|
<Button
|
||||||
|
onClick={() => performHttpAction(notification, action)}
|
||||||
|
aria-label={t("notifications_actions_http_request_title", { method: method, url: action.url })}
|
||||||
|
>{label}</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -416,7 +427,7 @@ const NoNotifications = (props) => {
|
||||||
return (
|
return (
|
||||||
<VerticallyCenteredContainer maxWidth="xs">
|
<VerticallyCenteredContainer maxWidth="xs">
|
||||||
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
||||||
<img src={logoOutline} height="64" width="64"/><br />
|
<img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")}/><br />
|
||||||
{t("notifications_none_for_topic_title")}
|
{t("notifications_none_for_topic_title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
|
@ -442,7 +453,7 @@ const NoNotificationsWithoutSubscription = (props) => {
|
||||||
return (
|
return (
|
||||||
<VerticallyCenteredContainer maxWidth="xs">
|
<VerticallyCenteredContainer maxWidth="xs">
|
||||||
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
||||||
<img src={logoOutline} height="64" width="64"/><br />
|
<img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")}/><br />
|
||||||
{t("notifications_none_for_any_title")}
|
{t("notifications_none_for_any_title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
|
@ -466,7 +477,7 @@ const NoSubscriptions = () => {
|
||||||
return (
|
return (
|
||||||
<VerticallyCenteredContainer maxWidth="xs">
|
<VerticallyCenteredContainer maxWidth="xs">
|
||||||
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
||||||
<img src={logoOutline} height="64" width="64"/><br />
|
<img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")}/><br />
|
||||||
{t("notifications_no_subscriptions_title")}
|
{t("notifications_no_subscriptions_title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
|
|
|
@ -34,11 +34,6 @@ import DialogActions from "@mui/material/DialogActions";
|
||||||
import userManager from "../app/UserManager";
|
import userManager from "../app/UserManager";
|
||||||
import {playSound, shuffle, sounds, validUrl} from "../app/utils";
|
import {playSound, shuffle, sounds, validUrl} from "../app/utils";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import priority1 from "../img/priority-1.svg";
|
|
||||||
import priority2 from "../img/priority-2.svg";
|
|
||||||
import priority3 from "../img/priority-3.svg";
|
|
||||||
import priority4 from "../img/priority-4.svg";
|
|
||||||
import priority5 from "../img/priority-5.svg";
|
|
||||||
|
|
||||||
const Preferences = () => {
|
const Preferences = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -55,7 +50,7 @@ const Preferences = () => {
|
||||||
const Notifications = () => {
|
const Notifications = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Card sx={{p: 3}}>
|
<Card sx={{p: 3}} aria-label={t("prefs_notifications_title")}>
|
||||||
<Typography variant="h5" sx={{marginBottom: 2}}>
|
<Typography variant="h5" sx={{marginBottom: 2}}>
|
||||||
{t("prefs_notifications_title")}
|
{t("prefs_notifications_title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@ -70,6 +65,7 @@ const Notifications = () => {
|
||||||
|
|
||||||
const Sound = () => {
|
const Sound = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const labelId = "prefSound";
|
||||||
const sound = useLiveQuery(async () => prefs.sound());
|
const sound = useLiveQuery(async () => prefs.sound());
|
||||||
const handleChange = async (ev) => {
|
const handleChange = async (ev) => {
|
||||||
await prefs.setSound(ev.target.value);
|
await prefs.setSound(ev.target.value);
|
||||||
|
@ -84,15 +80,15 @@ const Sound = () => {
|
||||||
description = t("prefs_notifications_sound_description_some", { sound: sounds[sound].label });
|
description = t("prefs_notifications_sound_description_some", { sound: sounds[sound].label });
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Pref title={t("prefs_notifications_sound_title")} description={description}>
|
<Pref labelId={labelId} title={t("prefs_notifications_sound_title")} description={description}>
|
||||||
<div style={{ display: 'flex', width: '100%' }}>
|
<div style={{ display: 'flex', width: '100%' }}>
|
||||||
<FormControl fullWidth variant="standard" sx={{ margin: 1 }}>
|
<FormControl fullWidth variant="standard" sx={{ margin: 1 }}>
|
||||||
<Select value={sound} onChange={handleChange}>
|
<Select value={sound} onChange={handleChange} aria-labelledby={labelId}>
|
||||||
<MenuItem value={"none"}>{t("prefs_notifications_sound_no_sound")}</MenuItem>
|
<MenuItem value={"none"}>{t("prefs_notifications_sound_no_sound")}</MenuItem>
|
||||||
{Object.entries(sounds).map(s => <MenuItem key={s[0]} value={s[0]}>{s[1].label}</MenuItem>)}
|
{Object.entries(sounds).map(s => <MenuItem key={s[0]} value={s[0]}>{s[1].label}</MenuItem>)}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<IconButton onClick={() => playSound(sound)} disabled={sound === "none"}>
|
<IconButton onClick={() => playSound(sound)} disabled={sound === "none"} aria-label={t("prefs_notifications_sound_play")}>
|
||||||
<PlayArrowIcon />
|
<PlayArrowIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -102,6 +98,7 @@ const Sound = () => {
|
||||||
|
|
||||||
const MinPriority = () => {
|
const MinPriority = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const labelId = "prefMinPriority";
|
||||||
const minPriority = useLiveQuery(async () => prefs.minPriority());
|
const minPriority = useLiveQuery(async () => prefs.minPriority());
|
||||||
const handleChange = async (ev) => {
|
const handleChange = async (ev) => {
|
||||||
await prefs.setMinPriority(ev.target.value);
|
await prefs.setMinPriority(ev.target.value);
|
||||||
|
@ -128,9 +125,9 @@ const MinPriority = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Pref title={t("prefs_notifications_min_priority_title")} description={description}>
|
<Pref labelId={labelId} title={t("prefs_notifications_min_priority_title")} description={description}>
|
||||||
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
|
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
|
||||||
<Select value={minPriority} onChange={handleChange}>
|
<Select value={minPriority} onChange={handleChange} aria-labelledby={labelId}>
|
||||||
<MenuItem value={1}>{t("prefs_notifications_min_priority_any")}</MenuItem>
|
<MenuItem value={1}>{t("prefs_notifications_min_priority_any")}</MenuItem>
|
||||||
<MenuItem value={2}>{t("prefs_notifications_min_priority_low_and_higher")}</MenuItem>
|
<MenuItem value={2}>{t("prefs_notifications_min_priority_low_and_higher")}</MenuItem>
|
||||||
<MenuItem value={3}>{t("prefs_notifications_min_priority_default_and_higher")}</MenuItem>
|
<MenuItem value={3}>{t("prefs_notifications_min_priority_default_and_higher")}</MenuItem>
|
||||||
|
@ -144,6 +141,7 @@ const MinPriority = () => {
|
||||||
|
|
||||||
const DeleteAfter = () => {
|
const DeleteAfter = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const labelId = "prefDeleteAfter";
|
||||||
const deleteAfter = useLiveQuery(async () => prefs.deleteAfter());
|
const deleteAfter = useLiveQuery(async () => prefs.deleteAfter());
|
||||||
const handleChange = async (ev) => {
|
const handleChange = async (ev) => {
|
||||||
await prefs.setDeleteAfter(ev.target.value);
|
await prefs.setDeleteAfter(ev.target.value);
|
||||||
|
@ -161,9 +159,9 @@ const DeleteAfter = () => {
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
return (
|
return (
|
||||||
<Pref title={t("prefs_notifications_delete_after_title")} description={description}>
|
<Pref labelId={labelId} title={t("prefs_notifications_delete_after_title")} description={description}>
|
||||||
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
|
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
|
||||||
<Select value={deleteAfter} onChange={handleChange}>
|
<Select value={deleteAfter} onChange={handleChange} aria-labelledby={labelId}>
|
||||||
<MenuItem value={0}>{t("prefs_notifications_delete_after_never")}</MenuItem>
|
<MenuItem value={0}>{t("prefs_notifications_delete_after_never")}</MenuItem>
|
||||||
<MenuItem value={10800}>{t("prefs_notifications_delete_after_three_hours")}</MenuItem>
|
<MenuItem value={10800}>{t("prefs_notifications_delete_after_three_hours")}</MenuItem>
|
||||||
<MenuItem value={86400}>{t("prefs_notifications_delete_after_one_day")}</MenuItem>
|
<MenuItem value={86400}>{t("prefs_notifications_delete_after_one_day")}</MenuItem>
|
||||||
|
@ -177,7 +175,7 @@ const DeleteAfter = () => {
|
||||||
|
|
||||||
const PrefGroup = (props) => {
|
const PrefGroup = (props) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div role="table">
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -185,28 +183,39 @@ const PrefGroup = (props) => {
|
||||||
|
|
||||||
const Pref = (props) => {
|
const Pref = (props) => {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div
|
||||||
|
role="row"
|
||||||
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
marginTop: "10px",
|
marginTop: "10px",
|
||||||
marginBottom: "20px",
|
marginBottom: "20px",
|
||||||
}}>
|
}}
|
||||||
<div style={{
|
>
|
||||||
|
<div
|
||||||
|
role="cell"
|
||||||
|
id={props.labelId}
|
||||||
|
aria-label={props.title}
|
||||||
|
style={{
|
||||||
flex: '1 0 40%',
|
flex: '1 0 40%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingRight: '30px'
|
paddingRight: '30px'
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<div><b>{props.title}</b></div>
|
<div><b>{props.title}</b></div>
|
||||||
{props.description && <div><em>{props.description}</em></div>}
|
{props.description && <div><em>{props.description}</em></div>}
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div
|
||||||
|
role="cell"
|
||||||
|
style={{
|
||||||
flex: '1 0 calc(60% - 50px)',
|
flex: '1 0 calc(60% - 50px)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -235,7 +244,7 @@ const Users = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Card sx={{ padding: 1 }}>
|
<Card sx={{ padding: 1 }} aria-label={t("prefs_users_title")}>
|
||||||
<CardContent sx={{ paddingBottom: 1 }}>
|
<CardContent sx={{ paddingBottom: 1 }}>
|
||||||
<Typography variant="h5" sx={{marginBottom: 2}}>
|
<Typography variant="h5" sx={{marginBottom: 2}}>
|
||||||
{t("prefs_users_title")}
|
{t("prefs_users_title")}
|
||||||
|
@ -291,7 +300,7 @@ const UserTable = (props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Table size="small">
|
<Table size="small" aria-label={t("prefs_users_table")}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell sx={{paddingLeft: 0}}>{t("prefs_users_table_user_header")}</TableCell>
|
<TableCell sx={{paddingLeft: 0}}>{t("prefs_users_table_user_header")}</TableCell>
|
||||||
|
@ -305,13 +314,13 @@ const UserTable = (props) => {
|
||||||
key={user.baseUrl}
|
key={user.baseUrl}
|
||||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row" sx={{paddingLeft: 0}}>{user.username}</TableCell>
|
<TableCell component="th" scope="row" sx={{paddingLeft: 0}} aria-label={t("prefs_users_table_user_header")}>{user.username}</TableCell>
|
||||||
<TableCell>{user.baseUrl}</TableCell>
|
<TableCell aria-label={t("prefs_users_table_base_url_header")}>{user.baseUrl}</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<IconButton onClick={() => handleEditClick(user)}>
|
<IconButton onClick={() => handleEditClick(user)} aria-label={t("prefs_users_edit_button")}>
|
||||||
<EditIcon/>
|
<EditIcon/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={() => handleDeleteClick(user)}>
|
<IconButton onClick={() => handleDeleteClick(user)} aria-label={t("prefs_users_delete_button")}>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
@ -371,6 +380,7 @@ const UserDialog = (props) => {
|
||||||
margin="dense"
|
margin="dense"
|
||||||
id="baseUrl"
|
id="baseUrl"
|
||||||
label={t("prefs_users_dialog_base_url_label")}
|
label={t("prefs_users_dialog_base_url_label")}
|
||||||
|
aria-label={t("prefs_users_dialog_base_url_label")}
|
||||||
value={baseUrl}
|
value={baseUrl}
|
||||||
onChange={ev => setBaseUrl(ev.target.value)}
|
onChange={ev => setBaseUrl(ev.target.value)}
|
||||||
type="url"
|
type="url"
|
||||||
|
@ -382,6 +392,7 @@ const UserDialog = (props) => {
|
||||||
margin="dense"
|
margin="dense"
|
||||||
id="username"
|
id="username"
|
||||||
label={t("prefs_users_dialog_username_label")}
|
label={t("prefs_users_dialog_username_label")}
|
||||||
|
aria-label={t("prefs_users_dialog_username_label")}
|
||||||
value={username}
|
value={username}
|
||||||
onChange={ev => setUsername(ev.target.value)}
|
onChange={ev => setUsername(ev.target.value)}
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -392,6 +403,7 @@ const UserDialog = (props) => {
|
||||||
margin="dense"
|
margin="dense"
|
||||||
id="password"
|
id="password"
|
||||||
label={t("prefs_users_dialog_password_label")}
|
label={t("prefs_users_dialog_password_label")}
|
||||||
|
aria-label={t("prefs_users_dialog_password_label")}
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={ev => setPassword(ev.target.value)}
|
onChange={ev => setPassword(ev.target.value)}
|
||||||
|
@ -410,7 +422,7 @@ const UserDialog = (props) => {
|
||||||
const Appearance = () => {
|
const Appearance = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Card sx={{p: 3}}>
|
<Card sx={{p: 3}} aria-label={t("prefs_appearance_title")}>
|
||||||
<Typography variant="h5" sx={{marginBottom: 2}}>
|
<Typography variant="h5" sx={{marginBottom: 2}}>
|
||||||
{t("prefs_appearance_title")}
|
{t("prefs_appearance_title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@ -423,6 +435,7 @@ const Appearance = () => {
|
||||||
|
|
||||||
const Language = () => {
|
const Language = () => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
const labelId = "prefLanguage";
|
||||||
const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
|
const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
|
||||||
const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" ");
|
const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" ");
|
||||||
const lang = i18n.language ?? "en";
|
const lang = i18n.language ?? "en";
|
||||||
|
@ -432,9 +445,9 @@ const Language = () => {
|
||||||
// Better: Sidebar in Wikipedia: https://en.wikipedia.org/wiki/Bokm%C3%A5l
|
// Better: Sidebar in Wikipedia: https://en.wikipedia.org/wiki/Bokm%C3%A5l
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pref title={title}>
|
<Pref labelId={labelId} title={title}>
|
||||||
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
|
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
|
||||||
<Select value={lang} onChange={(ev) => i18n.changeLanguage(ev.target.value)}>
|
<Select value={lang} onChange={(ev) => i18n.changeLanguage(ev.target.value)} aria-labelledby={labelId}>
|
||||||
<MenuItem value="en">English</MenuItem>
|
<MenuItem value="en">English</MenuItem>
|
||||||
<MenuItem value="bg">Български</MenuItem>
|
<MenuItem value="bg">Български</MenuItem>
|
||||||
<MenuItem value="cs">Čeština</MenuItem>
|
<MenuItem value="cs">Čeština</MenuItem>
|
||||||
|
|
|
@ -240,6 +240,7 @@ const PublishDialog = (props) => {
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
label={t("publish_dialog_base_url_label")}
|
label={t("publish_dialog_base_url_label")}
|
||||||
|
aria-label={t("publish_dialog_base_url_label")}
|
||||||
placeholder={t("publish_dialog_base_url_placeholder")}
|
placeholder={t("publish_dialog_base_url_placeholder")}
|
||||||
value={baseUrl}
|
value={baseUrl}
|
||||||
onChange={ev => setBaseUrl(ev.target.value)}
|
onChange={ev => setBaseUrl(ev.target.value)}
|
||||||
|
@ -251,6 +252,7 @@ const PublishDialog = (props) => {
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
label={t("publish_dialog_topic_label")}
|
label={t("publish_dialog_topic_label")}
|
||||||
|
aria-label={t("publish_dialog_topic_label")}
|
||||||
placeholder={t("publish_dialog_topic_placeholder")}
|
placeholder={t("publish_dialog_topic_placeholder")}
|
||||||
value={topic}
|
value={topic}
|
||||||
onChange={ev => setTopic(ev.target.value)}
|
onChange={ev => setTopic(ev.target.value)}
|
||||||
|
@ -265,6 +267,7 @@ const PublishDialog = (props) => {
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
label={t("publish_dialog_title_label")}
|
label={t("publish_dialog_title_label")}
|
||||||
|
aria-label={t("publish_dialog_title_label")}
|
||||||
placeholder={t("publish_dialog_title_placeholder")}
|
placeholder={t("publish_dialog_title_placeholder")}
|
||||||
value={title}
|
value={title}
|
||||||
onChange={ev => setTitle(ev.target.value)}
|
onChange={ev => setTitle(ev.target.value)}
|
||||||
|
@ -276,6 +279,7 @@ const PublishDialog = (props) => {
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
label={t("publish_dialog_message_label")}
|
label={t("publish_dialog_message_label")}
|
||||||
|
aria-label={t("publish_dialog_message_label")}
|
||||||
placeholder={t("publish_dialog_message_placeholder")}
|
placeholder={t("publish_dialog_message_placeholder")}
|
||||||
value={message}
|
value={message}
|
||||||
onChange={ev => setMessage(ev.target.value)}
|
onChange={ev => setMessage(ev.target.value)}
|
||||||
|
@ -293,12 +297,13 @@ const PublishDialog = (props) => {
|
||||||
onEmojiPick={handleEmojiPick}
|
onEmojiPick={handleEmojiPick}
|
||||||
onClose={handleEmojiClose}
|
onClose={handleEmojiClose}
|
||||||
/>
|
/>
|
||||||
<DialogIconButton disabled={disabled} onClick={handleEmojiClick}>
|
<DialogIconButton disabled={disabled} onClick={handleEmojiClick} aria-label={t("publish_dialog_emoji_picker_show")}>
|
||||||
<InsertEmoticonIcon/>
|
<InsertEmoticonIcon/>
|
||||||
</DialogIconButton>
|
</DialogIconButton>
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
label={t("publish_dialog_tags_label")}
|
label={t("publish_dialog_tags_label")}
|
||||||
|
aria-label={t("publish_dialog_tags_label")}
|
||||||
placeholder={t("publish_dialog_tags_placeholder")}
|
placeholder={t("publish_dialog_tags_placeholder")}
|
||||||
value={tags}
|
value={tags}
|
||||||
onChange={ev => setTags(ev.target.value)}
|
onChange={ev => setTags(ev.target.value)}
|
||||||
|
@ -315,15 +320,16 @@ const PublishDialog = (props) => {
|
||||||
<InputLabel/>
|
<InputLabel/>
|
||||||
<Select
|
<Select
|
||||||
label={t("publish_dialog_priority_label")}
|
label={t("publish_dialog_priority_label")}
|
||||||
|
aria-label={t("publish_dialog_priority_label")}
|
||||||
margin="dense"
|
margin="dense"
|
||||||
value={priority}
|
value={priority}
|
||||||
onChange={(ev) => setPriority(ev.target.value)}
|
onChange={(ev) => setPriority(ev.target.value)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{[5,4,3,2,1].map(priority =>
|
{[5,4,3,2,1].map(priority =>
|
||||||
<MenuItem key={`priorityMenuItem${priority}`} value={priority}>
|
<MenuItem key={`priorityMenuItem${priority}`} value={priority} aria-label={t("notifications_priority_x", { priority: priority })}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<img src={priorities[priority].file} style={{marginRight: "8px"}}/>
|
<img src={priorities[priority].file} style={{marginRight: "8px"}} alt={t("notifications_priority_x", { priority: priority })}/>
|
||||||
<div>{priorities[priority].label}</div>
|
<div>{priorities[priority].label}</div>
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -339,6 +345,7 @@ const PublishDialog = (props) => {
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
label={t("publish_dialog_click_label")}
|
label={t("publish_dialog_click_label")}
|
||||||
|
aria-label={t("publish_dialog_click_label")}
|
||||||
placeholder={t("publish_dialog_click_placeholder")}
|
placeholder={t("publish_dialog_click_placeholder")}
|
||||||
value={clickUrl}
|
value={clickUrl}
|
||||||
onChange={ev => setClickUrl(ev.target.value)}
|
onChange={ev => setClickUrl(ev.target.value)}
|
||||||
|
@ -357,6 +364,7 @@ const PublishDialog = (props) => {
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
label={t("publish_dialog_email_label")}
|
label={t("publish_dialog_email_label")}
|
||||||
|
aria-label={t("publish_dialog_email_label")}
|
||||||
placeholder={t("publish_dialog_email_placeholder")}
|
placeholder={t("publish_dialog_email_placeholder")}
|
||||||
value={email}
|
value={email}
|
||||||
onChange={ev => setEmail(ev.target.value)}
|
onChange={ev => setEmail(ev.target.value)}
|
||||||
|
@ -377,6 +385,7 @@ const PublishDialog = (props) => {
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
label={t("publish_dialog_attach_label")}
|
label={t("publish_dialog_attach_label")}
|
||||||
|
aria-label={t("publish_dialog_attach_label")}
|
||||||
placeholder={t("publish_dialog_attach_placeholder")}
|
placeholder={t("publish_dialog_attach_placeholder")}
|
||||||
value={attachUrl}
|
value={attachUrl}
|
||||||
onChange={ev => {
|
onChange={ev => {
|
||||||
|
@ -402,6 +411,7 @@ const PublishDialog = (props) => {
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
label={t("publish_dialog_filename_label")}
|
label={t("publish_dialog_filename_label")}
|
||||||
|
aria-label={t("publish_dialog_filename_label")}
|
||||||
placeholder={t("publish_dialog_filename_placeholder")}
|
placeholder={t("publish_dialog_filename_placeholder")}
|
||||||
value={filename}
|
value={filename}
|
||||||
onChange={ev => {
|
onChange={ev => {
|
||||||
|
@ -441,6 +451,7 @@ const PublishDialog = (props) => {
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
label={t("publish_dialog_delay_label")}
|
label={t("publish_dialog_delay_label")}
|
||||||
|
aria-label={t("publish_dialog_delay_label")}
|
||||||
placeholder={t("publish_dialog_delay_placeholder", {
|
placeholder={t("publish_dialog_delay_placeholder", {
|
||||||
unixTimestamp: "1649029748",
|
unixTimestamp: "1649029748",
|
||||||
relativeTime: "30m",
|
relativeTime: "30m",
|
||||||
|
@ -459,12 +470,12 @@ const PublishDialog = (props) => {
|
||||||
{t("publish_dialog_other_features")}
|
{t("publish_dialog_other_features")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<div>
|
<div>
|
||||||
{!showClickUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_click_label")} onClick={() => setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
{!showClickUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_click_label")} aria-label={t("publish_dialog_chip_click_label")} onClick={() => setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
||||||
{!showEmail && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_email_label")} onClick={() => setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
{!showEmail && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_email_label")} aria-label={t("publish_dialog_chip_email_label")} onClick={() => setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
||||||
{!showAttachUrl && !showAttachFile && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_url_label")} onClick={() => setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
{!showAttachUrl && !showAttachFile && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_url_label")} aria-label={t("publish_dialog_chip_attach_url_label")} onClick={() => setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
||||||
{!showAttachFile && !showAttachUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_file_label")} onClick={() => handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>}
|
{!showAttachFile && !showAttachUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_file_label")} aria-label={t("publish_dialog_chip_attach_file_label")} onClick={() => handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>}
|
||||||
{!showDelay && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_delay_label")} onClick={() => setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
{!showDelay && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_delay_label")} aria-label={t("publish_dialog_chip_delay_label")} onClick={() => setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
||||||
{!showTopicUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_topic_label")} onClick={() => setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
{!showTopicUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_topic_label")} aria-label={t("publish_dialog_chip_topic_label")} onClick={() => setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
||||||
</div>
|
</div>
|
||||||
<Typography variant="body1" sx={{marginTop: 1, marginBottom: 1}}>
|
<Typography variant="body1" sx={{marginTop: 1, marginBottom: 1}}>
|
||||||
<Trans
|
<Trans
|
||||||
|
@ -483,7 +494,13 @@ const PublishDialog = (props) => {
|
||||||
label={t("publish_dialog_checkbox_publish_another")}
|
label={t("publish_dialog_checkbox_publish_another")}
|
||||||
sx={{marginRight: 2}}
|
sx={{marginRight: 2}}
|
||||||
control={
|
control={
|
||||||
<Checkbox size="small" checked={publishAnother} onChange={(ev) => setPublishAnother(ev.target.checked)} />
|
<Checkbox
|
||||||
|
size="small"
|
||||||
|
checked={publishAnother}
|
||||||
|
onChange={(ev) => setPublishAnother(ev.target.checked)}
|
||||||
|
inputProps={{
|
||||||
|
"aria-label": t("publish_dialog_checkbox_publish_another")
|
||||||
|
}} />
|
||||||
} />
|
} />
|
||||||
<Button onClick={props.onClose}>{t("publish_dialog_button_cancel")}</Button>
|
<Button onClick={props.onClose}>{t("publish_dialog_button_cancel")}</Button>
|
||||||
<Button onClick={handleSubmit} disabled={!sendButtonEnabled}>{t("publish_dialog_button_send")}</Button>
|
<Button onClick={handleSubmit} disabled={!sendButtonEnabled}>{t("publish_dialog_button_send")}</Button>
|
||||||
|
@ -497,7 +514,7 @@ const PublishDialog = (props) => {
|
||||||
|
|
||||||
const Row = (props) => {
|
const Row = (props) => {
|
||||||
return (
|
return (
|
||||||
<div style={{display: 'flex'}}>
|
<div style={{display: 'flex'}} role="row">
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue