From 04955cc45ecfab8b9da5f17521acadc382a76c0e Mon Sep 17 00:00:00 2001
From: Mitch Lillie <mlillie87@gmail.com>
Date: Sat, 6 Feb 2021 12:20:41 -0800
Subject: [PATCH] Add aria attributes where possible (#156)

* Add aria attributes where possible

* Bump submodule to get aria translations
---
 lemmy-translations                            |  2 +-
 src/shared/components/banner-icon-header.tsx  |  3 +-
 src/shared/components/comment-node.tsx        | 30 ++++++++++++++++++-
 src/shared/components/communities.tsx         |  2 ++
 src/shared/components/image-upload-form.tsx   |  6 +++-
 src/shared/components/inbox.tsx               |  1 +
 src/shared/components/login.tsx               |  1 +
 src/shared/components/main.tsx                |  2 ++
 src/shared/components/markdown-textarea.tsx   |  8 +++++
 src/shared/components/password_change.tsx     |  6 ++--
 src/shared/components/pictrs-image.tsx        | 11 ++++++-
 src/shared/components/post-form.tsx           |  3 +-
 src/shared/components/post-listing.tsx        | 16 ++++++++++
 .../components/private-message-form.tsx       |  2 ++
 src/shared/components/private-message.tsx     | 14 +++++++++
 src/shared/components/search.tsx              |  8 +++--
 src/shared/components/sidebar.tsx             | 12 ++++++++
 src/shared/components/sort-select.tsx         |  5 ++--
 src/shared/components/user.tsx                | 24 ++++++++++-----
 19 files changed, 137 insertions(+), 19 deletions(-)

diff --git a/lemmy-translations b/lemmy-translations
index a47ae3f..084ce53 160000
--- a/lemmy-translations
+++ b/lemmy-translations
@@ -1 +1 @@
-Subproject commit a47ae3f825f74ad7a6106b92e21d3c66b10d3fe8
+Subproject commit 084ce539fff5253317d6460598b10a6867999c20
diff --git a/src/shared/components/banner-icon-header.tsx b/src/shared/components/banner-icon-header.tsx
index cb84eb2..050ace1 100644
--- a/src/shared/components/banner-icon-header.tsx
+++ b/src/shared/components/banner-icon-header.tsx
@@ -14,12 +14,13 @@ export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
   render() {
     return (
       <div class="position-relative mb-2">
-        {this.props.banner && <PictrsImage src={this.props.banner} />}
+        {this.props.banner && <PictrsImage src={this.props.banner} alt="" />}
         {this.props.icon && (
           <PictrsImage
             src={this.props.icon}
             iconOverlay
             pushup={!!this.props.banner}
+            alt=""
           />
         )}
       </div>
diff --git a/src/shared/components/comment-node.tsx b/src/shared/components/comment-node.tsx
index 071c238..26eeaf0 100644
--- a/src/shared/components/comment-node.tsx
+++ b/src/shared/components/comment-node.tsx
@@ -198,6 +198,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
               <button
                 class="btn btn-sm text-muted"
                 onClick={linkEvent(this, this.handleCommentCollapse)}
+                aria-label={
+                  this.state.collapsed ? i18n.t('expand') : i18n.t('collapse')
+                }
               >
                 {this.state.collapsed ? '+' : '—'}
               </button>
@@ -248,6 +251,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                           ? i18n.t('mark_as_unread')
                           : i18n.t('mark_as_read')
                       }
+                      aria-label={
+                        this.commentOrMentionRead
+                          ? i18n.t('mark_as_unread')
+                          : i18n.t('mark_as_read')
+                      }
                     >
                       {this.state.readLoading ? (
                         this.loadingIcon
@@ -270,6 +278,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                         }`}
                         onClick={linkEvent(node, this.handleCommentUpvote)}
                         data-tippy-content={i18n.t('upvote')}
+                        aria-label={i18n.t('upvote')}
                       >
                         <svg class="icon icon-inline">
                           <use xlinkHref="#icon-arrow-up1"></use>
@@ -287,6 +296,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                           }`}
                           onClick={linkEvent(node, this.handleCommentDownvote)}
                           data-tippy-content={i18n.t('downvote')}
+                          aria-label={i18n.t('downvote')}
                         >
                           <svg class="icon icon-inline">
                             <use xlinkHref="#icon-arrow-down1"></use>
@@ -300,6 +310,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                         class="btn btn-link btn-animate text-muted"
                         onClick={linkEvent(this, this.handleReplyClick)}
                         data-tippy-content={i18n.t('reply')}
+                        aria-label={i18n.t('reply')}
                       >
                         <svg class="icon icon-inline">
                           <use xlinkHref="#icon-reply1"></use>
@@ -310,6 +321,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                           className="btn btn-link btn-animate text-muted"
                           onClick={linkEvent(this, this.handleShowAdvanced)}
                           data-tippy-content={i18n.t('more')}
+                          aria-label={i18n.t('more')}
                         >
                           <svg class="icon icon-inline">
                             <use xlinkHref="#icon-more-vertical"></use>
@@ -340,6 +352,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                             data-tippy-content={
                               cv.saved ? i18n.t('unsave') : i18n.t('save')
                             }
+                            aria-label={
+                              cv.saved ? i18n.t('unsave') : i18n.t('save')
+                            }
                           >
                             {this.state.saveLoading ? (
                               this.loadingIcon
@@ -357,6 +372,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                             className="btn btn-link btn-animate text-muted"
                             onClick={linkEvent(this, this.handleViewSource)}
                             data-tippy-content={i18n.t('view_source')}
+                            aria-label={i18n.t('view_source')}
                           >
                             <svg
                               class={`icon icon-inline ${
@@ -372,6 +388,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                                 class="btn btn-link btn-animate text-muted"
                                 onClick={linkEvent(this, this.handleEditClick)}
                                 data-tippy-content={i18n.t('edit')}
+                                aria-label={i18n.t('edit')}
                               >
                                 <svg class="icon icon-inline">
                                   <use xlinkHref="#icon-edit"></use>
@@ -388,6 +405,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                                     ? i18n.t('delete')
                                     : i18n.t('restore')
                                 }
+                                aria-label={
+                                  !cv.comment.deleted
+                                    ? i18n.t('delete')
+                                    : i18n.t('restore')
+                                }
                               >
                                 <svg
                                   class={`icon icon-inline ${
@@ -667,9 +689,15 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
         {this.state.showBanDialog && (
           <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
             <div class="form-group row">
-              <label class="col-form-label">{i18n.t('reason')}</label>
+              <label
+                class="col-form-label"
+                htmlFor={`mod-ban-reason-${cv.comment.id}`}
+              >
+                {i18n.t('reason')}
+              </label>
               <input
                 type="text"
+                id={`mod-ban-reason-${cv.comment.id}`}
                 class="form-control mr-2"
                 placeholder={i18n.t('reason')}
                 value={this.state.banReason}
diff --git a/src/shared/components/communities.tsx b/src/shared/components/communities.tsx
index c852fe2..e90c09d 100644
--- a/src/shared/components/communities.tsx
+++ b/src/shared/components/communities.tsx
@@ -158,6 +158,7 @@ export class Communities extends Component<any, CommunitiesState> {
                         {cv.subscribed ? (
                           <span
                             class="pointer btn-link"
+                            role="button"
                             onClick={linkEvent(
                               cv.community.id,
                               this.handleUnsubscribe
@@ -168,6 +169,7 @@ export class Communities extends Component<any, CommunitiesState> {
                         ) : (
                           <span
                             class="pointer btn-link"
+                            role="button"
                             onClick={linkEvent(
                               cv.community.id,
                               this.handleSubscribe
diff --git a/src/shared/components/image-upload-form.tsx b/src/shared/components/image-upload-form.tsx
index c9ffea4..4b8866f 100644
--- a/src/shared/components/image-upload-form.tsx
+++ b/src/shared/components/image-upload-form.tsx
@@ -2,6 +2,7 @@ import { Component, linkEvent } from 'inferno';
 import { pictrsUri } from '../env';
 import { UserService } from '../services';
 import { toast, randomStr } from '../utils';
+import { i18n } from '../i18next';
 
 interface ImageUploadFormProps {
   uploadTitle: string;
@@ -48,7 +49,10 @@ export class ImageUploadForm extends Component<
                   this.props.rounded ? 'rounded-circle' : ''
                 }`}
               />
-              <a onClick={linkEvent(this, this.handleRemoveImage)}>
+              <a
+                onClick={linkEvent(this, this.handleRemoveImage)}
+                aria-label={i18n.t('remove')}
+              >
                 <svg class="icon mini-overlay">
                   <use xlinkHref="#icon-x"></use>
                 </svg>
diff --git a/src/shared/components/inbox.tsx b/src/shared/components/inbox.tsx
index 4390b8e..ab0c312 100644
--- a/src/shared/components/inbox.tsx
+++ b/src/shared/components/inbox.tsx
@@ -172,6 +172,7 @@ export class Inbox extends Component<any, InboxState> {
                     <li className="list-inline-item">
                       <span
                         class="pointer"
+                        role="button"
                         onClick={linkEvent(this, this.markAllAsRead)}
                       >
                         {i18n.t('mark_all_as_read')}
diff --git a/src/shared/components/login.tsx b/src/shared/components/login.tsx
index acd05a3..7006ae7 100644
--- a/src/shared/components/login.tsx
+++ b/src/shared/components/login.tsx
@@ -326,6 +326,7 @@ export class Login extends Component<any, State> {
               class="rounded-top img-fluid"
               src={this.captchaPngSrc()}
               style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
+              alt={i18n.t('captcha')}
             />
             {this.state.captcha.ok.wav && (
               <button
diff --git a/src/shared/components/main.tsx b/src/shared/components/main.tsx
index 1c7b7da..28795b5 100644
--- a/src/shared/components/main.tsx
+++ b/src/shared/components/main.tsx
@@ -508,7 +508,9 @@ export class Main extends Component<any, MainState> {
           <li className="list-inline-item-action">
             <span
               class="pointer"
+              role="button"
               onClick={linkEvent(this, this.handleEditClick)}
+              aria-label={i18n.t('edit')}
               data-tippy-content={i18n.t('edit')}
             >
               <svg class="icon icon-inline">
diff --git a/src/shared/components/markdown-textarea.tsx b/src/shared/components/markdown-textarea.tsx
index ded6dd1..cffc432 100644
--- a/src/shared/components/markdown-textarea.tsx
+++ b/src/shared/components/markdown-textarea.tsx
@@ -233,6 +233,7 @@ export class MarkdownTextArea extends Component<
             <button
               class="btn btn-sm text-muted"
               data-tippy-content={i18n.t('header')}
+              aria-label={i18n.t('header')}
               onClick={linkEvent(this, this.handleInsertHeader)}
             >
               <svg class="icon icon-inline">
@@ -242,6 +243,7 @@ export class MarkdownTextArea extends Component<
             <button
               class="btn btn-sm text-muted"
               data-tippy-content={i18n.t('strikethrough')}
+              aria-label={i18n.t('strikethrough')}
               onClick={linkEvent(this, this.handleInsertStrikethrough)}
             >
               <svg class="icon icon-inline">
@@ -251,6 +253,7 @@ export class MarkdownTextArea extends Component<
             <button
               class="btn btn-sm text-muted"
               data-tippy-content={i18n.t('quote')}
+              aria-label={i18n.t('quote')}
               onClick={linkEvent(this, this.handleInsertQuote)}
             >
               <svg class="icon icon-inline">
@@ -260,6 +263,7 @@ export class MarkdownTextArea extends Component<
             <button
               class="btn btn-sm text-muted"
               data-tippy-content={i18n.t('list')}
+              aria-label={i18n.t('list')}
               onClick={linkEvent(this, this.handleInsertList)}
             >
               <svg class="icon icon-inline">
@@ -269,6 +273,7 @@ export class MarkdownTextArea extends Component<
             <button
               class="btn btn-sm text-muted"
               data-tippy-content={i18n.t('code')}
+              aria-label={i18n.t('code')}
               onClick={linkEvent(this, this.handleInsertCode)}
             >
               <svg class="icon icon-inline">
@@ -278,6 +283,7 @@ export class MarkdownTextArea extends Component<
             <button
               class="btn btn-sm text-muted"
               data-tippy-content={i18n.t('subscript')}
+              aria-label={i18n.t('subscript')}
               onClick={linkEvent(this, this.handleInsertSubscript)}
             >
               <svg class="icon icon-inline">
@@ -287,6 +293,7 @@ export class MarkdownTextArea extends Component<
             <button
               class="btn btn-sm text-muted"
               data-tippy-content={i18n.t('superscript')}
+              aria-label={i18n.t('superscript')}
               onClick={linkEvent(this, this.handleInsertSuperscript)}
             >
               <svg class="icon icon-inline">
@@ -296,6 +303,7 @@ export class MarkdownTextArea extends Component<
             <button
               class="btn btn-sm text-muted"
               data-tippy-content={i18n.t('spoiler')}
+              aria-label={i18n.t('spoiler')}
               onClick={linkEvent(this, this.handleInsertSpoiler)}
             >
               <svg class="icon icon-inline">
diff --git a/src/shared/components/password_change.tsx b/src/shared/components/password_change.tsx
index 6ffa3e0..373d10f 100644
--- a/src/shared/components/password_change.tsx
+++ b/src/shared/components/password_change.tsx
@@ -80,11 +80,12 @@ export class PasswordChange extends Component<any, State> {
     return (
       <form onSubmit={linkEvent(this, this.handlePasswordChangeSubmit)}>
         <div class="form-group row">
-          <label class="col-sm-2 col-form-label">
+          <label class="col-sm-2 col-form-label" htmlFor="new-password">
             {i18n.t('new_password')}
           </label>
           <div class="col-sm-10">
             <input
+              id="new-password"
               type="password"
               value={this.state.passwordChangeForm.password}
               onInput={linkEvent(this, this.handlePasswordChange)}
@@ -94,11 +95,12 @@ export class PasswordChange extends Component<any, State> {
           </div>
         </div>
         <div class="form-group row">
-          <label class="col-sm-2 col-form-label">
+          <label class="col-sm-2 col-form-label" htmlFor="verify-password">
             {i18n.t('verify_password')}
           </label>
           <div class="col-sm-10">
             <input
+              id="verify-password"
               type="password"
               value={this.state.passwordChangeForm.password_verify}
               onInput={linkEvent(this, this.handleVerifyPasswordChange)}
diff --git a/src/shared/components/pictrs-image.tsx b/src/shared/components/pictrs-image.tsx
index b044721..1f5655d 100644
--- a/src/shared/components/pictrs-image.tsx
+++ b/src/shared/components/pictrs-image.tsx
@@ -6,6 +6,7 @@ const maxImageSize = 3000;
 
 interface PictrsImageProps {
   src: string;
+  alt?: string;
   icon?: boolean;
   thumbnail?: boolean;
   nsfw?: boolean;
@@ -25,13 +26,14 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
         <source srcSet={this.src('jpg')} type="image/jpeg" />
         <img
           src={this.src('jpg')}
+          alt={this.alt()}
           className={`
         ${!this.props.icon && !this.props.iconOverlay && 'img-fluid '}
         ${
           this.props.thumbnail && !this.props.icon
             ? 'thumbnail rounded '
             : 'img-expanded '
-        } 
+        }
         ${this.props.thumbnail && this.props.nsfw && 'img-blur '}
         ${this.props.icon && 'rounded-circle img-icon mr-2 '}
         ${this.props.iconOverlay && 'ml-2 mb-0 rounded-circle avatar-overlay '}
@@ -71,4 +73,11 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
 
     return out;
   }
+
+  alt(): string {
+    if (this.props.icon) {
+      return '';
+    }
+    return this.props.alt || '';
+  }
 }
diff --git a/src/shared/components/post-form.tsx b/src/shared/components/post-form.tsx
index 4295a62..7f5c7fb 100644
--- a/src/shared/components/post-form.tsx
+++ b/src/shared/components/post-form.tsx
@@ -180,6 +180,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
               {this.state.suggestedTitle && (
                 <div
                   class="mt-1 text-muted small font-weight-bold pointer"
+                  role="button"
                   onClick={linkEvent(this, this.copySuggestedTitle)}
                 >
                   {i18n.t('copy_suggested_title', {
@@ -227,7 +228,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
                 </svg>
               )}
               {isImage(this.state.postForm.url) && (
-                <img src={this.state.postForm.url} class="img-fluid" />
+                <img src={this.state.postForm.url} class="img-fluid" alt="" />
               )}
               {this.state.crossPosts.length > 0 && (
                 <>
diff --git a/src/shared/components/post-listing.tsx b/src/shared/components/post-listing.tsx
index d75584b..2d0192b 100644
--- a/src/shared/components/post-listing.tsx
+++ b/src/shared/components/post-listing.tsx
@@ -169,6 +169,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       <PictrsImage
         src={src}
         thumbnail
+        alt=""
         nsfw={post_view.post.nsfw || post_view.community.nsfw}
       />
     );
@@ -200,6 +201,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           class="float-right text-body pointer d-inline-block position-relative mb-2"
           data-tippy-content={i18n.t('expand_here')}
           onClick={linkEvent(this, this.handleImageExpandClick)}
+          role="button"
+          aria-label={i18n.t('expand_here')}
         >
           {this.imgThumb(this.getImageSrc())}
           <svg class="icon mini-overlay">
@@ -328,6 +331,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                   previewLines(post_view.post.body)
                 )}
                 data-tippy-allowHtml={true}
+                aria-label={i18n.t('upvote')}
                 to={`/post/${post_view.post.id}`}
               >
                 <svg class="mr-1 icon icon-inline">
@@ -350,6 +354,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           }`}
           onClick={linkEvent(this, this.handlePostLike)}
           data-tippy-content={i18n.t('upvote')}
+          aria-label={i18n.t('upvote')}
         >
           <svg class="icon upvote">
             <use xlinkHref="#icon-arrow-up1"></use>
@@ -368,6 +373,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
             }`}
             onClick={linkEvent(this, this.handlePostDisLike)}
             data-tippy-content={i18n.t('downvote')}
+            aria-label={i18n.t('downvote')}
           >
             <svg class="icon downvote">
               <use xlinkHref="#icon-arrow-down1"></use>
@@ -504,6 +510,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               <button
                 class="btn text-muted py-0 pr-0"
                 data-tippy-content={this.pointsTippy}
+                aria-label={i18n.t('downvote')}
               >
                 <small>
                   <svg class="icon icon-inline mr-1">
@@ -520,6 +527,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                 data-tippy-content={
                   post_view.saved ? i18n.t('unsave') : i18n.t('save')
                 }
+                aria-label={post_view.saved ? i18n.t('unsave') : i18n.t('save')}
               >
                 <small>
                   <svg
@@ -545,6 +553,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                 }`}
                 data-tippy-content={this.pointsTippy}
                 onClick={linkEvent(this, this.handlePostLike)}
+                aria-label={i18n.t('upvote')}
               >
                 <svg class="small icon icon-inline mr-2">
                   <use xlinkHref="#icon-arrow-up1"></use>
@@ -558,6 +567,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                   }`}
                   onClick={linkEvent(this, this.handlePostDisLike)}
                   data-tippy-content={this.pointsTippy}
+                  aria-label={i18n.t('downvote')}
                 >
                   <svg class="small icon icon-inline mr-2">
                     <use xlinkHref="#icon-arrow-down1"></use>
@@ -571,6 +581,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
             <button
               class="btn btn-link btn-animate text-muted py-0 pl-1 pr-0"
               onClick={linkEvent(this, this.handleSavePostClick)}
+              aria-label={post_view.saved ? i18n.t('unsave') : i18n.t('save')}
               data-tippy-content={
                 post_view.saved ? i18n.t('unsave') : i18n.t('save')
               }
@@ -586,6 +597,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               <button
                 class="btn btn-link btn-animate text-muted py-0"
                 onClick={linkEvent(this, this.handleShowMoreMobile)}
+                aria-label={i18n.t('more')}
                 data-tippy-content={i18n.t('more')}
               >
                 <svg class="icon icon-inline">
@@ -639,6 +651,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                   data-tippy-content={
                     post_view.saved ? i18n.t('unsave') : i18n.t('save')
                   }
+                  aria-label={
+                    post_view.saved ? i18n.t('unsave') : i18n.t('save')
+                  }
                 >
                   <svg
                     class={`icon icon-inline ${
@@ -694,6 +709,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               class="btn btn-link btn-animate text-muted py-0"
               onClick={linkEvent(this, this.handleShowAdvanced)}
               data-tippy-content={i18n.t('more')}
+              aria-label={i18n.t('more')}
             >
               <svg class="icon icon-inline">
                 <use xlinkHref="#icon-more-vertical"></use>
diff --git a/src/shared/components/private-message-form.tsx b/src/shared/components/private-message-form.tsx
index 5688869..b4a43a1 100644
--- a/src/shared/components/private-message-form.tsx
+++ b/src/shared/components/private-message-form.tsx
@@ -116,8 +116,10 @@ export class PrivateMessageForm extends Component<
               {i18n.t('message')}
               <span
                 onClick={linkEvent(this, this.handleShowDisclaimer)}
+                role="button"
                 class="ml-2 pointer text-danger"
                 data-tippy-content={i18n.t('disclaimer')}
+                aria-label={i18n.t('disclaimer')}
               >
                 <svg class={`icon icon-inline`}>
                   <use xlinkHref="#icon-alert-triangle"></use>
diff --git a/src/shared/components/private-message.tsx b/src/shared/components/private-message.tsx
index 0121ea8..7b22b74 100644
--- a/src/shared/components/private-message.tsx
+++ b/src/shared/components/private-message.tsx
@@ -77,6 +77,7 @@ export class PrivateMessage extends Component<
             </li>
             <li className="list-inline-item">
               <div
+                role="button"
                 className="pointer text-monospace"
                 onClick={linkEvent(this, this.handleMessageCollapse)}
               >
@@ -123,6 +124,11 @@ export class PrivateMessage extends Component<
                             ? i18n.t('mark_as_unread')
                             : i18n.t('mark_as_read')
                         }
+                        aria-label={
+                          message_view.private_message.read
+                            ? i18n.t('mark_as_unread')
+                            : i18n.t('mark_as_read')
+                        }
                       >
                         <svg
                           class={`icon icon-inline ${
@@ -138,6 +144,7 @@ export class PrivateMessage extends Component<
                         class="btn btn-link btn-animate text-muted"
                         onClick={linkEvent(this, this.handleReplyClick)}
                         data-tippy-content={i18n.t('reply')}
+                        aria-label={i18n.t('reply')}
                       >
                         <svg class="icon icon-inline">
                           <use xlinkHref="#icon-reply1"></use>
@@ -153,6 +160,7 @@ export class PrivateMessage extends Component<
                         class="btn btn-link btn-animate text-muted"
                         onClick={linkEvent(this, this.handleEditClick)}
                         data-tippy-content={i18n.t('edit')}
+                        aria-label={i18n.t('edit')}
                       >
                         <svg class="icon icon-inline">
                           <use xlinkHref="#icon-edit"></use>
@@ -168,6 +176,11 @@ export class PrivateMessage extends Component<
                             ? i18n.t('delete')
                             : i18n.t('restore')
                         }
+                        aria-label={
+                          !message_view.private_message.deleted
+                            ? i18n.t('delete')
+                            : i18n.t('restore')
+                        }
                       >
                         <svg
                           class={`icon icon-inline ${
@@ -186,6 +199,7 @@ export class PrivateMessage extends Component<
                     class="btn btn-link btn-animate text-muted"
                     onClick={linkEvent(this, this.handleViewSource)}
                     data-tippy-content={i18n.t('view_source')}
+                    aria-label={i18n.t('view_source')}
                   >
                     <svg
                       class={`icon icon-inline ${
diff --git a/src/shared/components/search.tsx b/src/shared/components/search.tsx
index f4ef430..1b61e3b 100644
--- a/src/shared/components/search.tsx
+++ b/src/shared/components/search.tsx
@@ -209,6 +209,7 @@ export class Search extends Component<any, SearchState> {
           class="form-control mr-2 mb-2"
           value={this.state.searchText}
           placeholder={`${i18n.t('search')}...`}
+          aria-label={i18n.t('search')}
           onInput={linkEvent(this, this.handleQChange)}
           required
           minLength={3}
@@ -233,8 +234,11 @@ export class Search extends Component<any, SearchState> {
           value={this.state.type_}
           onChange={linkEvent(this, this.handleTypeChange)}
           class="custom-select w-auto mb-2"
+          aria-label={i18n.t('type')}
         >
-          <option disabled>{i18n.t('type')}</option>
+          <option disabled aria-hidden="true">
+            {i18n.t('type')}
+          </option>
           <option value={SearchType.All}>{i18n.t('all')}</option>
           <option value={SearchType.Comments}>{i18n.t('comments')}</option>
           <option value={SearchType.Posts}>{i18n.t('posts')}</option>
@@ -382,7 +386,7 @@ export class Search extends Component<any, SearchState> {
         <span>
           <CommunityLink community={community_view.community} />
         </span>
-        <span>{` - 
+        <span>{` -
         ${i18n.t('number_of_subscribers', {
           count: community_view.counts.subscribers,
         })}
diff --git a/src/shared/components/sidebar.tsx b/src/shared/components/sidebar.tsx
index 03675dd..db96efa 100644
--- a/src/shared/components/sidebar.tsx
+++ b/src/shared/components/sidebar.tsx
@@ -299,9 +299,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
             <>
               <li className="list-inline-item-action">
                 <span
+                  role="button"
                   class="pointer"
                   onClick={linkEvent(this, this.handleEditClick)}
                   data-tippy-content={i18n.t('edit')}
+                  aria-label={i18n.t('edit')}
                 >
                   <svg class="icon icon-inline">
                     <use xlinkHref="#icon-edit"></use>
@@ -313,6 +315,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
                   <li className="list-inline-item-action">
                     <span
                       class="pointer"
+                      role="button"
                       onClick={linkEvent(
                         this,
                         this.handleShowConfirmLeaveModTeamClick
@@ -329,6 +332,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
                     <li className="list-inline-item-action">
                       <span
                         class="pointer"
+                        role="button"
                         onClick={linkEvent(this, this.handleLeaveModTeamClick)}
                       >
                         {i18n.t('yes')}
@@ -337,6 +341,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
                     <li className="list-inline-item-action">
                       <span
                         class="pointer"
+                        role="button"
                         onClick={linkEvent(
                           this,
                           this.handleCancelLeaveModTeamClick
@@ -357,6 +362,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
                         ? i18n.t('delete')
                         : i18n.t('restore')
                     }
+                    aria-label={
+                      !community_view.community.deleted
+                        ? i18n.t('delete')
+                        : i18n.t('restore')
+                    }
                   >
                     <svg
                       class={`icon icon-inline ${
@@ -375,6 +385,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
               {!this.props.community_view.community.removed ? (
                 <span
                   class="pointer"
+                  role="button"
                   onClick={linkEvent(this, this.handleModRemoveShow)}
                 >
                   {i18n.t('remove')}
@@ -382,6 +393,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
               ) : (
                 <span
                   class="pointer"
+                  role="button"
                   onClick={linkEvent(this, this.handleModRemoveSubmit)}
                 >
                   {i18n.t('restore')}
diff --git a/src/shared/components/sort-select.tsx b/src/shared/components/sort-select.tsx
index ef14a6e..5ed2bc6 100644
--- a/src/shared/components/sort-select.tsx
+++ b/src/shared/components/sort-select.tsx
@@ -40,8 +40,9 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
           value={this.state.sort}
           onChange={linkEvent(this, this.handleSortChange)}
           class="custom-select w-auto mr-2 mb-2"
+          aria-label={i18n.t('sort_type')}
         >
-          <option disabled>{i18n.t('sort_type')}</option>
+          <option disabled aria-hidden="true">{i18n.t('sort_type')}</option>
           {!this.props.hideHot && [
             <option value={SortType.Hot}>{i18n.t('hot')}</option>,
             <option value={SortType.Active}>{i18n.t('active')}</option>,
@@ -52,7 +53,7 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
               {i18n.t('most_comments')}
             </option>
           )}
-          <option disabled>─────</option>
+          <option disabled aria-hidden="true">─────</option>
           <option value={SortType.TopDay}>{i18n.t('top_day')}</option>
           <option value={SortType.TopWeek}>{i18n.t('top_week')}</option>
           <option value={SortType.TopMonth}>{i18n.t('top_month')}</option>
diff --git a/src/shared/components/user.tsx b/src/shared/components/user.tsx
index e838851..0dbe252 100644
--- a/src/shared/components/user.tsx
+++ b/src/shared/components/user.tsx
@@ -526,28 +526,36 @@ export class User extends Component<any, UserState> {
                 />
               </div>
               <div class="form-group">
-                <label>{i18n.t('language')}</label>
+                <label htmlFor="user-language">{i18n.t('language')}</label>
                 <select
+                  id="user-language"
                   value={this.state.userSettingsForm.lang}
                   onChange={linkEvent(this, this.handleUserSettingsLangChange)}
                   class="ml-2 custom-select w-auto"
                 >
-                  <option disabled>{i18n.t('language')}</option>
+                  <option disabled aria-hidden="true">
+                    {i18n.t('language')}
+                  </option>
                   <option value="browser">{i18n.t('browser_default')}</option>
-                  <option disabled>──</option>
+                  <option disabled aria-hidden="true">
+                    ──
+                  </option>
                   {languages.map(lang => (
                     <option value={lang.code}>{lang.name}</option>
                   ))}
                 </select>
               </div>
               <div class="form-group">
-                <label>{i18n.t('theme')}</label>
+                <label htmlFor="user-theme">{i18n.t('theme')}</label>
                 <select
+                  id="user-theme"
                   value={this.state.userSettingsForm.theme}
                   onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
                   class="ml-2 custom-select w-auto"
                 >
-                  <option disabled>{i18n.t('theme')}</option>
+                  <option disabled aria-hidden="true">
+                    {i18n.t('theme')}
+                  </option>
                   <option value="browser">{i18n.t('browser_default')}</option>
                   {themes.map(theme => (
                     <option value={theme}>{theme}</option>
@@ -584,11 +592,12 @@ export class User extends Component<any, UserState> {
                 />
               </form>
               <div class="form-group row">
-                <label class="col-lg-5 col-form-label">
+                <label class="col-lg-5 col-form-label" htmlFor="display-name">
                   {i18n.t('display_name')}
                 </label>
                 <div class="col-lg-7">
                   <input
+                    id="display-name"
                     type="text"
                     class="form-control"
                     placeholder={i18n.t('optional')}
@@ -636,13 +645,14 @@ export class User extends Component<any, UserState> {
                 </div>
               </div>
               <div class="form-group row">
-                <label class="col-lg-5 col-form-label">
+                <label class="col-lg-5 col-form-label" htmlFor="matrix-user-id">
                   <a href={elementUrl} target="_blank" rel="noopener">
                     {i18n.t('matrix_user_id')}
                   </a>
                 </label>
                 <div class="col-lg-7">
                   <input
+                    id="matrix-user-id"
                     type="text"
                     class="form-control"
                     placeholder="@user:example.com"
-- 
2.44.1