From 0094cbb7d1ba2deb7f3fea3f65e140dc5c8ff3ac Mon Sep 17 00:00:00 2001
From: Agate <me@agate.blue>
Date: Fri, 3 Jul 2020 16:07:44 +0200
Subject: [PATCH] Fix #1128: Lock focus in modals to improve accessibility

---
 changes/changelog.d/1121.enhancement            |  1 +
 front/package.json                              |  1 +
 front/src/components/Queue.vue                  |  5 ++++-
 front/src/components/SetInstanceModal.vue       |  2 +-
 front/src/components/ShortcutsModal.vue         |  2 +-
 front/src/components/auth/LoginForm.vue         |  3 +--
 front/src/components/common/DangerousButton.vue |  8 ++++----
 front/src/components/moderation/FilterModal.vue |  4 ++--
 front/src/components/moderation/ReportModal.vue |  2 +-
 front/src/components/semantic/Modal.vue         | 12 +++++++++++-
 front/yarn.lock                                 | 13 +++++++++++++
 11 files changed, 40 insertions(+), 13 deletions(-)
 create mode 100644 changes/changelog.d/1121.enhancement

diff --git a/changes/changelog.d/1121.enhancement b/changes/changelog.d/1121.enhancement
new file mode 100644
index 000000000..188865c94
--- /dev/null
+++ b/changes/changelog.d/1121.enhancement
@@ -0,0 +1 @@
+Use semantic headers for accessibility (#1121)
\ No newline at end of file
diff --git a/front/package.json b/front/package.json
index 4a85f6469..86540e149 100644
--- a/front/package.json
+++ b/front/package.json
@@ -20,6 +20,7 @@
     "core-js": "^3.6.4",
     "diff": "^4.0.1",
     "django-channels": "^1.1.6",
+    "focus-trap": "^5.1.0",
     "fomantic-ui-css": "^2.8.3",
     "howler": "^2.0.14",
     "js-logger": "^1.4.1",
diff --git a/front/src/components/Queue.vue b/front/src/components/Queue.vue
index 4becb11f3..0813291af 100644
--- a/front/src/components/Queue.vue
+++ b/front/src/components/Queue.vue
@@ -209,7 +209,7 @@ import $ from 'jquery'
 import moment from "moment"
 import lodash from '@/lodash'
 import time from "@/utils/time"
-
+import createFocusTrap from 'focus-trap'
 import store from "@/store"
 
 export default {
@@ -224,11 +224,14 @@ export default {
       showVolume: false,
       isShuffling: false,
       tracksChangeBuffer: null,
+      focusTrap: null,
       time
     }
   },
   mounted () {
     let self = this
+    this.focusTrap = createFocusTrap(this.$el, {allowOutsideClick: () => { return true }})
+    this.focusTrap.activate()
     this.$nextTick(() => {
       setTimeout(() => {
         this.scrollToCurrent()
diff --git a/front/src/components/SetInstanceModal.vue b/front/src/components/SetInstanceModal.vue
index 618a16a20..2e5c24bc2 100644
--- a/front/src/components/SetInstanceModal.vue
+++ b/front/src/components/SetInstanceModal.vue
@@ -35,7 +35,7 @@
       </form>
     </div>
     <div class="actions">
-      <div class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></div>
+      <button class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></button>
     </div>
   </modal>
 </template>
diff --git a/front/src/components/ShortcutsModal.vue b/front/src/components/ShortcutsModal.vue
index 017cdc977..c88bfb715 100644
--- a/front/src/components/ShortcutsModal.vue
+++ b/front/src/components/ShortcutsModal.vue
@@ -36,7 +36,7 @@
       </div>
     </section>
     <footer class="actions">
-      <div class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Close</translate></div>
+      <button class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Close</translate></button>
     </footer>
   </modal>
 </template>
diff --git a/front/src/components/auth/LoginForm.vue b/front/src/components/auth/LoginForm.vue
index 3b18fbc29..f0591a538 100644
--- a/front/src/components/auth/LoginForm.vue
+++ b/front/src/components/auth/LoginForm.vue
@@ -25,7 +25,6 @@
         </label>
         <input
         ref="username"
-        tabindex="1"
         required
         name="username"
         type="text"
@@ -50,7 +49,7 @@
         <translate translate-context="Contant/Auth/Paragraph" :translate-params="{domain: $store.getters['instance/domain']}">You will be redirected to %{ domain } to authenticate.</translate>
       </p>
     </template>
-    <button tabindex="3" :class="['ui', {'loading': isLoading}, 'right', 'floated', buttonClasses, 'button']" type="submit">
+    <button :class="['ui', {'loading': isLoading}, 'right', 'floated', buttonClasses, 'button']" type="submit">
       <translate translate-context="*/Login/*/Verb">Login</translate>
     </button>
   </form>
diff --git a/front/src/components/common/DangerousButton.vue b/front/src/components/common/DangerousButton.vue
index 353c8ecf3..763f3fa98 100644
--- a/front/src/components/common/DangerousButton.vue
+++ b/front/src/components/common/DangerousButton.vue
@@ -14,14 +14,14 @@
         </div>
       </div>
       <div class="actions">
-        <div class="ui basic cancel button">
+        <button class="ui basic cancel button">
           <translate translate-context="*/*/Button.Label/Verb">Cancel</translate>
-        </div>
-        <div :class="['ui', 'confirm', confirmButtonColor, 'button']" @click="confirm">
+        </button>
+        <button :class="['ui', 'confirm', confirmButtonColor, 'button']" @click="confirm">
           <slot name="modal-confirm">
             <translate translate-context="Modal/*/Button.Label/Short, Verb">Confirm</translate>
           </slot>
-        </div>
+        </button>
       </div>
     </modal>
   </div>
diff --git a/front/src/components/moderation/FilterModal.vue b/front/src/components/moderation/FilterModal.vue
index 2c650014c..f83e69741 100644
--- a/front/src/components/moderation/FilterModal.vue
+++ b/front/src/components/moderation/FilterModal.vue
@@ -37,8 +37,8 @@
       </div>
     </div>
     <div class="actions">
-      <div class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></div>
-      <div :class="['ui', 'success', {loading: isLoading}, 'button']" @click="hide"><translate translate-context="Popup/*/Button.Label">Hide content</translate></div>
+      <button class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></button>
+      <button :class="['ui', 'success', {loading: isLoading}, 'button']" @click="hide"><translate translate-context="Popup/*/Button.Label">Hide content</translate></button>
     </div>
   </modal>
 </template>
diff --git a/front/src/components/moderation/ReportModal.vue b/front/src/components/moderation/ReportModal.vue
index db37960a2..f32f7b6f6 100644
--- a/front/src/components/moderation/ReportModal.vue
+++ b/front/src/components/moderation/ReportModal.vue
@@ -71,7 +71,7 @@
       </div>
     </div>
     <div class="actions">
-      <div class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></div>
+      <button class="ui basic cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></button>
       <button
         v-if="canSubmit"
         :class="['ui', 'success', {loading: isLoading}, 'button']"
diff --git a/front/src/components/semantic/Modal.vue b/front/src/components/semantic/Modal.vue
index 78f953421..0a93a053e 100644
--- a/front/src/components/semantic/Modal.vue
+++ b/front/src/components/semantic/Modal.vue
@@ -9,6 +9,7 @@
 
 <script>
 import $ from 'jquery'
+import createFocusTrap from 'focus-trap'
 
 export default {
   props: {
@@ -17,9 +18,13 @@ export default {
   },
   data () {
     return {
-      control: null
+      control: null,
+      focusTrap: null,
     }
   },
+  mounted () {
+    this.focusTrap = createFocusTrap(this.$el)
+  },
   beforeDestroy () {
     if (this.control) {
       $(this.$el).modal('hide')
@@ -38,6 +43,11 @@ export default {
         }.bind(this),
         onHidden: function () {
           this.$emit('update:show', false)
+          this.focusTrap.pause()
+        }.bind(this),
+        onVisible: function () {
+          this.focusTrap.activate()
+          this.focusTrap.unpause()
         }.bind(this)
       })
     }
diff --git a/front/yarn.lock b/front/yarn.lock
index 40aa5b9d0..4cab2dee1 100644
--- a/front/yarn.lock
+++ b/front/yarn.lock
@@ -4375,6 +4375,14 @@ flush-write-stream@^1.0.0:
     inherits "^2.0.3"
     readable-stream "^2.3.6"
 
+focus-trap@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-5.1.0.tgz#64a0bfabd95c382103397dbc96bfef3a3cf8e5ad"
+  integrity sha512-CkB/nrO55069QAUjWFBpX6oc+9V90Qhgpe6fBWApzruMq5gnlh90Oo7iSSDK7pKiV5ugG6OY2AXM5mxcmL3lwQ==
+  dependencies:
+    tabbable "^4.0.0"
+    xtend "^4.0.1"
+
 follow-redirects@1.5.10:
   version "1.5.10"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
@@ -9206,6 +9214,11 @@ symbol-tree@^3.2.2:
   resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
   integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
 
+tabbable@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-4.0.0.tgz#5bff1d1135df1482cf0f0206434f15eadbeb9261"
+  integrity sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==
+
 table@^5.2.3:
   version "5.4.6"
   resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
-- 
GitLab