Forráskód Böngészése

Make screenreaders read out autocomplete results

tusooa 2 éve
szülő
commit
6235af4592

+ 35 - 18
src/components/emoji_input/emoji_input.js

@@ -1,6 +1,7 @@
 import Completion from '../../services/completion/completion.js'
 import EmojiPicker from '../emoji_picker/emoji_picker.vue'
 import Popover from 'src/components/popover/popover.vue'
+import ScreenReaderNotice from 'src/components/screen_reader_notice/screen_reader_notice.vue'
 import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
 import { take } from 'lodash'
 import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
@@ -109,9 +110,10 @@ const EmojiInput = {
   },
   data () {
     return {
+      randomSeed: `${Math.random()}`.replace('.', '-'),
       input: undefined,
       caretEl: undefined,
-      highlighted: 0,
+      highlighted: -1,
       caret: 0,
       focused: false,
       blurTimeout: null,
@@ -125,7 +127,8 @@ const EmojiInput = {
   components: {
     Popover,
     EmojiPicker,
-    UnicodeDomainIndicator
+    UnicodeDomainIndicator,
+    ScreenReaderNotice
   },
   computed: {
     padEmoji () {
@@ -203,6 +206,12 @@ const EmojiInput = {
         top: this.input.scrollTop,
         left: this.input.scrollLeft
       })
+    },
+    suggestionListId () {
+      return `suggestions-${this.randomSeed}`
+    },
+    suggestionItemId () {
+      return (index) => `suggestion-item-${index}-${this.randomSeed}`
     }
   },
   mounted () {
@@ -278,6 +287,10 @@ const EmojiInput = {
           ...rest,
           img: imageUrl || ''
         }))
+      this.$refs.screenReaderNotice.announce(
+        this.$tc('tool_tip.autocomplete_available',
+          this.suggestions.length,
+          { number: this.suggestions.length }))
     }
   },
   methods: {
@@ -374,27 +387,24 @@ const EmojiInput = {
     },
     cycleBackward (e) {
       const len = this.suggestions.length || 0
-      if (len > 1) {
-        this.highlighted -= 1
-        if (this.highlighted < 0) {
-          this.highlighted = this.suggestions.length - 1
-        }
-        e.preventDefault()
-      } else {
-        this.highlighted = 0
+
+      this.highlighted -= 1
+      if (this.highlighted === -1) {
+        this.input.focus()
+      } else if (this.highlighted < -1) {
+        this.highlighted = len - 1
       }
+      e.preventDefault()
     },
     cycleForward (e) {
       const len = this.suggestions.length || 0
-      if (len > 1) {
-        this.highlighted += 1
-        if (this.highlighted >= len) {
-          this.highlighted = 0
-        }
-        e.preventDefault()
-      } else {
-        this.highlighted = 0
+
+      this.highlighted += 1
+      if (this.highlighted >= len) {
+        this.highlighted = -1
+        this.input.focus()
       }
+      e.preventDefault()
     },
     scrollIntoView () {
       const rootRef = this.$refs.picker.$el
@@ -540,6 +550,13 @@ const EmojiInput = {
       })
     },
     resize () {
+    },
+    autoCompleteItemLabel (suggestion) {
+      if (suggestion.user) {
+        return suggestion.displayText + ' ' + suggestion.detailText
+      } else {
+        return this.maybeLocalizedEmojiName(suggestion)
+      }
     }
   }
 }

+ 17 - 2
src/components/emoji_input/emoji_input.vue

@@ -4,7 +4,13 @@
     class="emoji-input"
     :class="{ 'with-picker': !hideEmojiButton }"
   >
-    <slot />
+    <slot
+      :id="'textbox-' + randomSeed"
+      :aria-owns="suggestionListId"
+      aria-autocomplete="both"
+      :aria-expanded="showSuggestions"
+      :aria-activedescendant="(!showSuggestions || highlighted === -1) ? '' : suggestionItemId(highlighted)"
+    />
     <!-- TODO: make the 'x' disappear if at the end maybe? -->
     <div
       ref="hiddenOverlay"
@@ -18,6 +24,10 @@
       >x</span>
       <span>{{ postText }}</span>
     </div>
+    <screen-reader-notice
+      ref="screenReaderNotice"
+      aria-live="assertive"
+    />
     <template v-if="enableEmojiPicker">
       <button
         v-if="!hideEmojiButton"
@@ -46,15 +56,20 @@
     >
       <template #content>
         <div
+          :id="suggestionListId"
           ref="panel-body"
           class="autocomplete-panel-body"
+          role="listbox"
         >
           <div
             v-for="(suggestion, index) in suggestions"
+            :id="suggestionItemId(index)"
             :key="index"
             class="autocomplete-item"
-            role="button"
+            role="option"
             :class="{ highlighted: index === highlighted }"
+            :aria-label="autoCompleteItemLabel(suggestion)"
+            :aria-selected="index === highlighted"
             @click.stop.prevent="onClick($event, suggestion)"
           >
             <span class="image">

+ 30 - 23
src/components/post_status_form/post_status_form.vue

@@ -148,29 +148,36 @@
           @sticker-upload-failed="uploadFailed"
           @shown="handleEmojiInputShow"
         >
-          <textarea
-            ref="textarea"
-            v-model="newStatus.status"
-            :placeholder="placeholder || $t('post_status.default')"
-            rows="1"
-            cols="1"
-            :disabled="posting && !optimisticPosting"
-            class="form-post-body"
-            :class="{ 'scrollable-form': !!maxHeight }"
-            @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
-            @keydown.meta.enter="postStatus($event, newStatus)"
-            @keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
-            @input="resize"
-            @compositionupdate="resize"
-            @paste="paste"
-          />
-          <p
-            v-if="hasStatusLengthLimit"
-            class="character-counter faint"
-            :class="{ error: isOverLengthLimit }"
-          >
-            {{ charactersLeft }}
-          </p>
+          <template #default="inputProps">
+            <textarea
+              ref="textarea"
+              v-model="newStatus.status"
+              :placeholder="placeholder || $t('post_status.default')"
+              rows="1"
+              cols="1"
+              :disabled="posting && !optimisticPosting"
+              class="form-post-body"
+              :class="{ 'scrollable-form': !!maxHeight }"
+              v-bind="inputProps"
+              :aria-owns="inputProps.ariaOwns"
+              :aria-autocomplete="inputProps.ariaAutocomplete"
+              :aria-activedescendant="inputProps.ariaActiveDescendant"
+              :aria-expanded="inputProps.ariaExpanded"
+              @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
+              @keydown.meta.enter="postStatus($event, newStatus)"
+              @keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
+              @input="resize"
+              @compositionupdate="resize"
+              @paste="paste"
+            />
+            <p
+              v-if="hasStatusLengthLimit"
+              class="character-counter faint"
+              :class="{ error: isOverLengthLimit }"
+            >
+              {{ charactersLeft }}
+            </p>
+          </template>
         </EmojiInput>
         <div
           v-if="!disableScopeSelector"

+ 21 - 0
src/components/screen_reader_notice/screen_reader_notice.js

@@ -0,0 +1,21 @@
+const ScreenReaderNotice = {
+  props: {
+    ariaLive: {
+      type: String,
+      defualt: 'assertive'
+    }
+  },
+  data () {
+    return {
+      currentText: ''
+    }
+  },
+  methods: {
+    announce (text) {
+      this.currentText = text
+      setTimeout(() => { this.currentText = '' }, 1000)
+    }
+  }
+}
+
+export default ScreenReaderNotice

+ 21 - 0
src/components/screen_reader_notice/screen_reader_notice.vue

@@ -0,0 +1,21 @@
+<template>
+  <div
+    class="screen-reader-text"
+    :aria-live="ariaLive"
+  >
+    {{ currentText }}
+  </div>
+</template>
+
+<script src="./screen_reader_notice.js"></script>
+
+<style lang="scss">
+.screen-reader-text {
+  display: block;
+  width: 1px;
+  height: 1px;
+  margin: -1px;
+  overflow: hidden;
+  visibility: visible;
+}
+</style>

+ 2 - 1
src/i18n/en.json

@@ -996,7 +996,8 @@
     "reject_follow_request": "Reject follow request",
     "bookmark": "Bookmark",
     "toggle_expand": "Expand or collapse notification to show post in full",
-    "toggle_mute": "Expand or collapse notification to reveal muted content"
+    "toggle_mute": "Expand or collapse notification to reveal muted content",
+    "autocomplete_available": "{number} result is available | {number} results are available"
   },
   "upload": {
     "error": {