diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index ad05f57b..c97859cc 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -1,10 +1,16 @@ { + "action_bar_show_menu": "Show menu", + "action_bar_logo_alt": "ntfy logo", "action_bar_settings": "Settings", "action_bar_send_test_notification": "Send test notification", "action_bar_clear_notifications": "Clear all notifications", "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_error_publishing": "Error publishing notification", + "message_bar_show_dialog": "Show publish dialog", + "message_bar_publish": "Publish message", "nav_topics_title": "Subscribed topics", "nav_button_all_notifications": "All notifications", "nav_button_settings": "Settings", @@ -16,14 +22,25 @@ "alert_grant_button": "Grant now", "alert_not_supported_title": "Notifications not supported", "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_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_button": "Copy URL", "notifications_attachment_open_title": "Go to {{url}}", "notifications_attachment_open_button": "Open attachment", "notifications_attachment_link_expires": "link expires {{date}}", "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_button": "Copy 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_reached": "exceeds {{fileSizeLimit}} file limit", "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_low": "Low priority", "publish_dialog_priority_default": "Default priority", @@ -89,6 +107,7 @@ "publish_dialog_attached_file_filename_placeholder": "Attachment filename", "publish_dialog_drop_file_here": "Drop file here", "emoji_picker_search_placeholder": "Search emoji", + "emoji_picker_search_clear": "Clear search", "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_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_some": "Notifications play the {{sound}} sound when they arrive", "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_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", @@ -130,7 +150,10 @@ "prefs_notifications_delete_after_one_month_description": "Notifications are auto-deleted after one month", "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_table": "Users table", "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_base_url_header": "Service URL", "prefs_users_dialog_title_add": "Add user", diff --git a/web/src/components/ActionBar.js b/web/src/components/ActionBar.js index 95e14440..30ab271e 100644 --- a/web/src/components/ActionBar.js +++ b/web/src/components/ActionBar.js @@ -44,16 +44,22 @@ const ActionBar = (props) => { <IconButton color="inherit" edge="start" + aria-label={t("action_bar_show_menu")} onClick={props.onMobileDrawerToggle} sx={{ mr: 2, display: { sm: 'none' } }} > <MenuIcon /> </IconButton> - <Box component="img" src={logo} sx={{ - display: { xs: 'none', sm: 'block' }, - marginRight: '10px', - height: '28px' - }}/> + <Box + component="img" + src={logo} + alt={t("action_bar_logo_alt")} + sx={{ + display: { xs: 'none', sm: 'block' }, + marginRight: '10px', + height: '28px' + }} + /> <Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}> {title} </Typography> @@ -173,10 +179,10 @@ const SettingsIcons = (props) => { 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/>} </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/> </IconButton> <Popper diff --git a/web/src/components/AttachmentIcon.js b/web/src/components/AttachmentIcon.js index 30e82273..337760b7 100644 --- a/web/src/components/AttachmentIcon.js +++ b/web/src/components/AttachmentIcon.js @@ -5,27 +5,36 @@ import fileImage from "../img/file-image.svg"; import fileVideo from "../img/file-video.svg"; import fileAudio from "../img/file-audio.svg"; import fileApp from "../img/file-app.svg"; +import {useTranslation} from "react-i18next"; const AttachmentIcon = (props) => { + const { t } = useTranslation(); const type = props.type; - let imageFile; + let imageFile, imageLabel; if (!type) { imageFile = fileDocument; + imageLabel = t("notifications_attachment_file_image"); } else if (type.startsWith('image/')) { imageFile = fileImage; + imageLabel = t("notifications_attachment_file_video"); } else if (type.startsWith('video/')) { imageFile = fileVideo; + imageLabel = t("notifications_attachment_file_video"); } else if (type.startsWith('audio/')) { imageFile = fileAudio; + imageLabel = t("notifications_attachment_file_audio"); } else if (type === "application/vnd.android.package-archive") { imageFile = fileApp; + imageLabel = t("notifications_attachment_file_app"); } else { imageFile = fileDocument; + imageLabel = t("notifications_attachment_file_document"); } return ( <Box component="img" src={imageFile} + alt={imageLabel} loading="lazy" sx={{ width: '28px', diff --git a/web/src/components/EmojiPicker.js b/web/src/components/EmojiPicker.js index 1392d7e8..9e5da67f 100644 --- a/web/src/components/EmojiPicker.js +++ b/web/src/components/EmojiPicker.js @@ -73,6 +73,8 @@ const EmojiPicker = (props) => { inputRef={searchRef} margin="dense" size="small" + role="searchbox" + aria-label={t("emoji_picker_search_placeholder")} placeholder={t("emoji_picker_search_placeholder")} value={search} onChange={ev => setSearch(ev.target.value)} @@ -83,7 +85,9 @@ const EmojiPicker = (props) => { InputProps={{ endAdornment: <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> }} /> @@ -130,10 +134,12 @@ const Category = (props) => { const Emoji = (props) => { const emoji = props.emoji; const matches = emojiMatches(emoji, props.search); + const title = `${emoji.description} (${emoji.aliases[0]})`; return ( <EmojiDiv onClick={props.onClick} - title={`${emoji.description} (${emoji.aliases[0]})`} + title={title} + aria-label={title} style={{ display: (matches) ? '' : 'none' }} > {props.emoji.emoji} diff --git a/web/src/components/Messaging.js b/web/src/components/Messaging.js index b4418459..4ba1203f 100644 --- a/web/src/components/Messaging.js +++ b/web/src/components/Messaging.js @@ -75,13 +75,15 @@ const MessageBar = (props) => { 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/> </IconButton> <TextField autoFocus margin="dense" placeholder={t("message_bar_type_message")} + aria-label={t("message_bar_type_message")} + role="textbox" type="text" fullWidth 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/> </IconButton> <Portal> diff --git a/web/src/components/Navigation.js b/web/src/components/Navigation.js index 513930b7..b5a9fb19 100644 --- a/web/src/components/Navigation.js +++ b/web/src/components/Navigation.js @@ -31,10 +31,15 @@ const navWidth = 280; const Navigation = (props) => { const navigationList = <NavList {...props}/>; 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 */} <Drawer variant="temporary" + role="menubar" open={props.mobileDrawerOpen} onClose={props.onMobileDrawerToggle} ModalProps={{ keepMounted: true }} // Better open performance on mobile. @@ -49,6 +54,7 @@ const Navigation = (props) => { <Drawer open variant="permanent" + role="menubar" sx={{ display: { xs: 'none', sm: 'block' }, '& .MuiDrawer-paper': { boxSizing: 'border-box', width: navWidth }, diff --git a/web/src/components/Notifications.js b/web/src/components/Notifications.js index 3a952190..b0bbae82 100644 --- a/web/src/components/Notifications.js +++ b/web/src/components/Notifications.js @@ -98,6 +98,8 @@ const NotificationList = (props) => { > <Container maxWidth="md" + role="list" + aria-label={t("notifications_list")} sx={{ marginTop: 3, 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 showActions = hasAttachmentActions || hasClickAction || hasUserActions; return ( - <Card sx={{ minWidth: 275, padding: 1 }}> + <Card sx={{ minWidth: 275, padding: 1 }} role="listitem" aria-label={t("notifications_list_item")}> <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 /> </IconButton> <Typography sx={{ fontSize: 14 }} color="text.secondary"> @@ -153,15 +155,15 @@ const NotificationItem = (props) => { {[1,2,4,5].includes(notification.priority) && <img src={priorityFiles[notification.priority]} - alt={`Priority ${notification.priority}`} + alt={t("notifications_priority_x", { priority: notification.priority})} style={{ verticalAlign: 'bottom' }} />} {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"/> </svg>} </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' }}> {autolink(maybeAppendActionErrors(formatMessage(notification), notification))} </Typography> @@ -289,6 +291,7 @@ const Attachment = (props) => { }; const Image = (props) => { + const { t } = useTranslation(); const [open, setOpen] = useState(false); return ( <> @@ -296,6 +299,7 @@ const Image = (props) => { component="img" src={props.attachment.url} loading="lazy" + alt={t("notifications_attachment_image")} onClick={() => setOpen(true)} sx={{ marginTop: 2, @@ -316,6 +320,7 @@ const Image = (props) => { <Box component="img" src={props.attachment.url} + alt={t("notifications_attachment_image")} loading="lazy" sx={{ maxWidth: 1, @@ -347,13 +352,16 @@ const UserAction = (props) => { if (action.action === "broadcast") { return ( <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> ); } else if (action.action === "view") { return ( <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> ); } else if (action.action === "http") { @@ -361,7 +369,10 @@ const UserAction = (props) => { const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? ""); return ( <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> ); } @@ -416,7 +427,7 @@ const NoNotifications = (props) => { return ( <VerticallyCenteredContainer maxWidth="xs"> <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")} </Typography> <Paragraph> @@ -442,7 +453,7 @@ const NoNotificationsWithoutSubscription = (props) => { return ( <VerticallyCenteredContainer maxWidth="xs"> <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")} </Typography> <Paragraph> @@ -466,7 +477,7 @@ const NoSubscriptions = () => { return ( <VerticallyCenteredContainer maxWidth="xs"> <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")} </Typography> <Paragraph> diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js index 117f11d1..7d320e39 100644 --- a/web/src/components/Preferences.js +++ b/web/src/components/Preferences.js @@ -34,11 +34,6 @@ import DialogActions from "@mui/material/DialogActions"; import userManager from "../app/UserManager"; import {playSound, shuffle, sounds, validUrl} from "../app/utils"; 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 = () => { return ( @@ -55,7 +50,7 @@ const Preferences = () => { const Notifications = () => { const { t } = useTranslation(); return ( - <Card sx={{p: 3}}> + <Card sx={{p: 3}} aria-label={t("prefs_notifications_title")}> <Typography variant="h5" sx={{marginBottom: 2}}> {t("prefs_notifications_title")} </Typography> @@ -70,6 +65,7 @@ const Notifications = () => { const Sound = () => { const { t } = useTranslation(); + const labelId = "prefSound"; const sound = useLiveQuery(async () => prefs.sound()); const handleChange = async (ev) => { await prefs.setSound(ev.target.value); @@ -84,15 +80,15 @@ const Sound = () => { description = t("prefs_notifications_sound_description_some", { sound: sounds[sound].label }); } 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%' }}> <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> {Object.entries(sounds).map(s => <MenuItem key={s[0]} value={s[0]}>{s[1].label}</MenuItem>)} </Select> </FormControl> - <IconButton onClick={() => playSound(sound)} disabled={sound === "none"}> + <IconButton onClick={() => playSound(sound)} disabled={sound === "none"} aria-label={t("prefs_notifications_sound_play")}> <PlayArrowIcon /> </IconButton> </div> @@ -102,6 +98,7 @@ const Sound = () => { const MinPriority = () => { const { t } = useTranslation(); + const labelId = "prefMinPriority"; const minPriority = useLiveQuery(async () => prefs.minPriority()); const handleChange = async (ev) => { await prefs.setMinPriority(ev.target.value); @@ -128,9 +125,9 @@ const MinPriority = () => { }); } 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 }}> - <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={2}>{t("prefs_notifications_min_priority_low_and_higher")}</MenuItem> <MenuItem value={3}>{t("prefs_notifications_min_priority_default_and_higher")}</MenuItem> @@ -144,6 +141,7 @@ const MinPriority = () => { const DeleteAfter = () => { const { t } = useTranslation(); + const labelId = "prefDeleteAfter"; const deleteAfter = useLiveQuery(async () => prefs.deleteAfter()); const handleChange = async (ev) => { await prefs.setDeleteAfter(ev.target.value); @@ -161,9 +159,9 @@ const DeleteAfter = () => { } })(); 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 }}> - <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={10800}>{t("prefs_notifications_delete_after_three_hours")}</MenuItem> <MenuItem value={86400}>{t("prefs_notifications_delete_after_one_day")}</MenuItem> @@ -177,7 +175,7 @@ const DeleteAfter = () => { const PrefGroup = (props) => { return ( - <div> + <div role="table"> {props.children} </div> ) @@ -185,28 +183,39 @@ const PrefGroup = (props) => { const Pref = (props) => { return ( - <div style={{ - display: "flex", - flexDirection: "row", - marginTop: "10px", - marginBottom: "20px", - }}> - <div style={{ - flex: '1 0 40%', - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - paddingRight: '30px' - }}> + <div + role="row" + style={{ + display: "flex", + flexDirection: "row", + marginTop: "10px", + marginBottom: "20px", + }} + > + <div + role="cell" + id={props.labelId} + aria-label={props.title} + style={{ + flex: '1 0 40%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + paddingRight: '30px' + }} + > <div><b>{props.title}</b></div> {props.description && <div><em>{props.description}</em></div>} </div> - <div style={{ - flex: '1 0 calc(60% - 50px)', - display: 'flex', - flexDirection: 'column', - justifyContent: 'center' - }}> + <div + role="cell" + style={{ + flex: '1 0 calc(60% - 50px)', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center' + }} + > {props.children} </div> </div> @@ -235,7 +244,7 @@ const Users = () => { } }; return ( - <Card sx={{ padding: 1 }}> + <Card sx={{ padding: 1 }} aria-label={t("prefs_users_title")}> <CardContent sx={{ paddingBottom: 1 }}> <Typography variant="h5" sx={{marginBottom: 2}}> {t("prefs_users_title")} @@ -291,7 +300,7 @@ const UserTable = (props) => { } }; return ( - <Table size="small"> + <Table size="small" aria-label={t("prefs_users_table")}> <TableHead> <TableRow> <TableCell sx={{paddingLeft: 0}}>{t("prefs_users_table_user_header")}</TableCell> @@ -305,13 +314,13 @@ const UserTable = (props) => { key={user.baseUrl} sx={{ '&:last-child td, &:last-child th': { border: 0 } }} > - <TableCell component="th" scope="row" sx={{paddingLeft: 0}}>{user.username}</TableCell> - <TableCell>{user.baseUrl}</TableCell> + <TableCell component="th" scope="row" sx={{paddingLeft: 0}} aria-label={t("prefs_users_table_user_header")}>{user.username}</TableCell> + <TableCell aria-label={t("prefs_users_table_base_url_header")}>{user.baseUrl}</TableCell> <TableCell align="right"> - <IconButton onClick={() => handleEditClick(user)}> + <IconButton onClick={() => handleEditClick(user)} aria-label={t("prefs_users_edit_button")}> <EditIcon/> </IconButton> - <IconButton onClick={() => handleDeleteClick(user)}> + <IconButton onClick={() => handleDeleteClick(user)} aria-label={t("prefs_users_delete_button")}> <CloseIcon /> </IconButton> </TableCell> @@ -371,6 +380,7 @@ const UserDialog = (props) => { margin="dense" id="baseUrl" label={t("prefs_users_dialog_base_url_label")} + aria-label={t("prefs_users_dialog_base_url_label")} value={baseUrl} onChange={ev => setBaseUrl(ev.target.value)} type="url" @@ -382,6 +392,7 @@ const UserDialog = (props) => { margin="dense" id="username" label={t("prefs_users_dialog_username_label")} + aria-label={t("prefs_users_dialog_username_label")} value={username} onChange={ev => setUsername(ev.target.value)} type="text" @@ -392,6 +403,7 @@ const UserDialog = (props) => { margin="dense" id="password" label={t("prefs_users_dialog_password_label")} + aria-label={t("prefs_users_dialog_password_label")} type="password" value={password} onChange={ev => setPassword(ev.target.value)} @@ -410,7 +422,7 @@ const UserDialog = (props) => { const Appearance = () => { const { t } = useTranslation(); return ( - <Card sx={{p: 3}}> + <Card sx={{p: 3}} aria-label={t("prefs_appearance_title")}> <Typography variant="h5" sx={{marginBottom: 2}}> {t("prefs_appearance_title")} </Typography> @@ -423,6 +435,7 @@ const Appearance = () => { const Language = () => { const { t, i18n } = useTranslation(); + const labelId = "prefLanguage"; const randomFlags = shuffle(["๐ฌ๐ง", "๐บ๐ธ", "๐ช๐ธ", "๐ซ๐ท", "๐ง๐ฌ", "๐จ๐ฟ", "๐ฉ๐ช", "๐ฎ๐ฉ", "๐ฏ๐ต", "๐ท๐บ", "๐น๐ท"]).slice(0, 3); const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" "); const lang = i18n.language ?? "en"; @@ -432,9 +445,9 @@ const Language = () => { // Better: Sidebar in Wikipedia: https://en.wikipedia.org/wiki/Bokm%C3%A5l return ( - <Pref title={title}> + <Pref labelId={labelId} title={title}> <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="bg">ะัะปะณะฐััะบะธ</MenuItem> <MenuItem value="cs">ฤeลกtina</MenuItem> diff --git a/web/src/components/PublishDialog.js b/web/src/components/PublishDialog.js index 00ba3d14..d78e67b8 100644 --- a/web/src/components/PublishDialog.js +++ b/web/src/components/PublishDialog.js @@ -240,6 +240,7 @@ const PublishDialog = (props) => { <TextField margin="dense" label={t("publish_dialog_base_url_label")} + aria-label={t("publish_dialog_base_url_label")} placeholder={t("publish_dialog_base_url_placeholder")} value={baseUrl} onChange={ev => setBaseUrl(ev.target.value)} @@ -251,6 +252,7 @@ const PublishDialog = (props) => { <TextField margin="dense" label={t("publish_dialog_topic_label")} + aria-label={t("publish_dialog_topic_label")} placeholder={t("publish_dialog_topic_placeholder")} value={topic} onChange={ev => setTopic(ev.target.value)} @@ -265,6 +267,7 @@ const PublishDialog = (props) => { <TextField margin="dense" label={t("publish_dialog_title_label")} + aria-label={t("publish_dialog_title_label")} placeholder={t("publish_dialog_title_placeholder")} value={title} onChange={ev => setTitle(ev.target.value)} @@ -276,6 +279,7 @@ const PublishDialog = (props) => { <TextField margin="dense" label={t("publish_dialog_message_label")} + aria-label={t("publish_dialog_message_label")} placeholder={t("publish_dialog_message_placeholder")} value={message} onChange={ev => setMessage(ev.target.value)} @@ -293,12 +297,13 @@ const PublishDialog = (props) => { onEmojiPick={handleEmojiPick} onClose={handleEmojiClose} /> - <DialogIconButton disabled={disabled} onClick={handleEmojiClick}> + <DialogIconButton disabled={disabled} onClick={handleEmojiClick} aria-label={t("publish_dialog_emoji_picker_show")}> <InsertEmoticonIcon/> </DialogIconButton> <TextField margin="dense" label={t("publish_dialog_tags_label")} + aria-label={t("publish_dialog_tags_label")} placeholder={t("publish_dialog_tags_placeholder")} value={tags} onChange={ev => setTags(ev.target.value)} @@ -315,15 +320,16 @@ const PublishDialog = (props) => { <InputLabel/> <Select label={t("publish_dialog_priority_label")} + aria-label={t("publish_dialog_priority_label")} margin="dense" value={priority} onChange={(ev) => setPriority(ev.target.value)} disabled={disabled} > {[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' }}> - <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> </MenuItem> @@ -339,6 +345,7 @@ const PublishDialog = (props) => { <TextField margin="dense" label={t("publish_dialog_click_label")} + aria-label={t("publish_dialog_click_label")} placeholder={t("publish_dialog_click_placeholder")} value={clickUrl} onChange={ev => setClickUrl(ev.target.value)} @@ -357,6 +364,7 @@ const PublishDialog = (props) => { <TextField margin="dense" label={t("publish_dialog_email_label")} + aria-label={t("publish_dialog_email_label")} placeholder={t("publish_dialog_email_placeholder")} value={email} onChange={ev => setEmail(ev.target.value)} @@ -377,6 +385,7 @@ const PublishDialog = (props) => { <TextField margin="dense" label={t("publish_dialog_attach_label")} + aria-label={t("publish_dialog_attach_label")} placeholder={t("publish_dialog_attach_placeholder")} value={attachUrl} onChange={ev => { @@ -402,6 +411,7 @@ const PublishDialog = (props) => { <TextField margin="dense" label={t("publish_dialog_filename_label")} + aria-label={t("publish_dialog_filename_label")} placeholder={t("publish_dialog_filename_placeholder")} value={filename} onChange={ev => { @@ -441,6 +451,7 @@ const PublishDialog = (props) => { <TextField margin="dense" label={t("publish_dialog_delay_label")} + aria-label={t("publish_dialog_delay_label")} placeholder={t("publish_dialog_delay_placeholder", { unixTimestamp: "1649029748", relativeTime: "30m", @@ -459,12 +470,12 @@ const PublishDialog = (props) => { {t("publish_dialog_other_features")} </Typography> <div> - {!showClickUrl && <Chip clickable disabled={disabled} 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}}/>} - {!showAttachUrl && !showAttachFile && <Chip clickable disabled={disabled} 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}}/>} - {!showDelay && <Chip clickable disabled={disabled} 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}}/>} + {!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")} 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")} 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")} 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")} 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")} aria-label={t("publish_dialog_chip_topic_label")} onClick={() => setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} </div> <Typography variant="body1" sx={{marginTop: 1, marginBottom: 1}}> <Trans @@ -483,7 +494,13 @@ const PublishDialog = (props) => { label={t("publish_dialog_checkbox_publish_another")} sx={{marginRight: 2}} 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={handleSubmit} disabled={!sendButtonEnabled}>{t("publish_dialog_button_send")}</Button> @@ -497,7 +514,7 @@ const PublishDialog = (props) => { const Row = (props) => { return ( - <div style={{display: 'flex'}}> + <div style={{display: 'flex'}} role="row"> {props.children} </div> );