浏览代码

防止朗读竞争

任一存 3 年之前
父节点
当前提交
9e90bdc0e4
共有 3 个文件被更改,包括 103 次插入83 次删除
  1. 6 6
      web/README.md
  2. 10 10
      web/src/assets/css/ariaGlobalStyle.less
  3. 87 67
      web/src/views/accessibility.vue

+ 6 - 6
web/README.md

@@ -27,23 +27,23 @@ See [Configuration Reference](https://cli.vuejs.org/config/).
 * 2.tab键或程序调用focus方法(由于按下快捷键或者在连读模式),focus到页面特定区域,朗读页面特定区域概述(结构读屏)
 * 3.tab键或程序调用focus方法(由于按下快捷键或者在连读模式),focus到元素,朗读元素内容
 * 4.主动hover到元素,朗读元素内容(有延时)
-* 5.点击或回车触发无障碍菜单按钮,朗读刚刚发生的行为。
+* 5.点击或回车触发无障碍菜单按钮,朗读刚刚发生的行为。
 
 ### 要避免的行为
-* 点击导致的focus,不可有反应。(记录点击行为的最后一次时间,此后极短时间内focus事件里不朗读,且如果focus到了页面特殊区域(页面结构元素),还要取消focus。)(额外添加的tabindex=-1的元素不需要了,样式的特殊处理也不需要了。)
-* 页面更新导致被动hover到元素,不可有反应。(记录页面更新的最后一次时间,此后极短时间内hover事件不理会
+* 点击导致的focus,不可有反应。(记录点击行为的最后一次时间,此后极短时间内focus事件里不朗读,且如果focus到了页面特殊区域(页面结构元素),还要取消focus。)(完成)(额外添加的tabindex=-1的元素不需要了,样式的特殊处理也不需要了。)
+* 页面更新导致被动hover到元素,不可有反应。(不能用上一次mousemove事件的时间来判断是否要响应hover事件,因为mousemove到按钮上马上点击,也可能导致被动hover。正确做法是根据页面最后一次更新的时间来判断是否要响应hover事件。判断页面更新:用mutation observer观察document.body
 
 ### 竞争情况(不考虑极端操作时偶尔的竞争。)
 * 1与2:不会竞争
 * 1与3:不会竞争
-* 1与4:会竞争
-* 1与5:点击help按钮时会竞争。改为点击help按钮时不朗读,help页加载后朗读刚刚发生的行为。(已完成)
+* 1与4:会竞争。移动到某个元素上立刻点击导致页面更新时。改为请求朗读后短时间内不理会hover事件。
+* 1与5:点击help按钮时会竞争。改为点击help按钮时不朗读,help页加载后朗读刚刚发生的行为。
 * 2与3:不会竞争
 * 2与4:不会竞争
 * 2与5:不会竞争
 * 3与4:不会竞争
 * 3与5:不会竞争
-* 4与5:会竞争。点击无障碍菜单按钮后极短时间内hover事件不理会
+* 4与5:会竞争。移动到某个按钮上立刻点击时。改为请求朗读后短时间内不理会hover事件
 
 ## 特殊class
 * aria-control-target: 手动添加。此节点和其后代会受无障碍菜单的控制。

+ 10 - 10
web/src/assets/css/ariaGlobalStyle.less

@@ -2,11 +2,11 @@
 
 .aria-active {
   .aria-theme-default {
-    &:focus:not([tabindex='-1']) {
+    &:focus {
       outline: 3px solid red;
     }
     * {
-      &:focus:not([tabindex='-1']) {
+      &:focus {
         outline: 3px solid red;
       }
     }
@@ -15,13 +15,13 @@
   .aria-theme-white {
     background-color: white !important;
     color: black !important;
-    &:focus:not([tabindex='-1']) {
+    &:focus {
       outline: 3px solid black;
     }
     * { // TODO: 看看博物馆项目代码有没有精细地只给需要变色的元素添加class。
       background-color: white !important;
       color: black !important;
-      &:focus:not([tabindex='-1']) {
+      &:focus {
         outline: 3px solid black;
       }
     }
@@ -41,13 +41,13 @@
   .aria-theme-blue {
     background-color: blue !important;
     color: yellow !important;
-    &:focus:not([tabindex='-1']) {
+    &:focus {
       outline: 3px solid yellow;
     }
     * {
       background-color: blue !important;
       color: yellow !important;
-      &:focus:not([tabindex='-1']) {
+      &:focus {
         outline: 3px solid yellow;
       }
     }
@@ -67,13 +67,13 @@
   .aria-theme-yellow {
     background-color: yellow !important;
     color: black !important;
-    &:focus:not([tabindex='-1']) {
+    &:focus {
       outline: 3px solid black;
     }
     * {
       background-color: yellow !important;
       color: black !important;
-      &:focus:not([tabindex='-1']) {
+      &:focus {
         outline: 3px solid black;
       }
     }
@@ -93,13 +93,13 @@
   .aria-theme-black {
     background-color: black !important;
     color: yellow !important;
-    &:focus:not([tabindex='-1']) {
+    &:focus {
       outline: 3px solid yellow;
     }
     * {
       background-color: black !important;
       color: yellow !important;
-      &:focus:not([tabindex='-1']) {
+      &:focus {
         outline: 3px solid yellow;
       }
     }

+ 87 - 67
web/src/views/accessibility.vue

@@ -476,11 +476,11 @@ export default {
       viewportAreaNum: null,
       interactionAreaNum: null,
 
-      focusedNode: null,
-      focusedNodeTimeoutId: null,
-
-      // 做有些操作如新打开页面、切换无障碍菜单模式时,会被动地导致mouseover事件,此时不希望做响应。
-      ignoreMouseOver: false,
+      // 为了避免朗读竞争
+      isJustMouseDown: false,
+      domChangeLastTime: null,
+      mutationObserver: null,
+      requestReadLastTime: null,
     }
   },
   watch: {
@@ -617,11 +617,6 @@ export default {
     },
   },
   created() {
-    this.ignoreMouseOver = true
-    setTimeout(() => {
-      this.ignoreMouseOver = false
-    }, 1000);
-    
     this.loadStoredSettings()
     // 在同一个域的多个页面间做同步
     window.addEventListener('storage', this.loadStoredSettings, {
@@ -633,10 +628,20 @@ export default {
     document.addEventListener('focusin', this.onFocusIn, {
       passive: true,
     })
+    document.addEventListener('mousedown', this.onMouseDown, {
+      passive: true,
+    })
+    document.addEventListener('mouseover', this.onMouseOver, {
+      passive: true,
+    })
+    document.addEventListener("visibilitychange", this.onPageVisibilityChange, {
+      passive: true
+    })
 
     this.$eventBus.$on('request-read', (text) => {
       console.log('无障碍组件收到request-read消息:', text);
       if (this.ariaSettings.isCompActive) {
+        this.requestReadLastTime = Date.now()
         this.planToPlayAudio('', text)
       }
     })
@@ -662,9 +667,12 @@ export default {
       }
     })
 
-    document.addEventListener("visibilitychange", this.onPageVisibilityChange, {
-      passive: true
-    })
+    const config = { attributes: true, childList: true, subtree: true };
+    const callback = (mutationsList, observer) => {
+      this.domChangeLastTime = Date.now()
+    };
+    this.mutationObserver = new MutationObserver(callback);
+    this.mutationObserver.observe(document.body, config);
   },
   mounted() {
   },
@@ -678,20 +686,24 @@ export default {
     document.removeEventListener('focusin', this.onFocusIn, {
       passive: true,
     })
-
-    this.$eventBus.$off('request-read')
-    this.$eventBus.$off('request-magnify')
-    this.$eventBus.$off('request-process-text-element')
-
     document.removeEventListener('mouseover', this.onMouseOverForContinueRead, {
       passive: true,
     })
     document.removeEventListener('mouseover', this.onMouseOverForPointRead, {
       passive: true,
     })
+    document.removeEventListener('mousedown', this.onMouseDown, {
+      passive: true,
+    })
     document.removeEventListener("visibilitychange", this.onPageVisibilityChange, {
       passive: true
     })
+
+    this.$eventBus.$off('request-read')
+    this.$eventBus.$off('request-magnify')
+    this.$eventBus.$off('request-process-text-element')
+
+    this.mutationObserver.disconnect();
   },
   methods: {
     onPageVisibilityChange() {
@@ -701,6 +713,12 @@ export default {
         }
       }
     },
+    onMouseDown() {
+      this.isJustMouseDown = true
+      setTimeout(() => {
+        this.isJustMouseDown = false
+      }, 0);
+    },
     planToPlayAudio(taskId, text = '') {
       let XHR = new XMLHttpRequest()
       const that = this
@@ -856,7 +874,13 @@ export default {
       if (!this.ariaSettings.isCompActive) {
         return
       }
-      if (this.ignoreMouseOver) {
+      const curTime = Date.now()
+      if (curTime - this.domChangeLastTime <= 500 + 100) {
+        console.log('DOM刚改变,忽略hover。');
+        return
+      }
+      if (curTime - this.requestReadLastTime <= 500 + 100) {
+        console.log('刚被要求朗读,忽略hover。');
         return
       }
       const extractedText = utils.extractTextForMagnify(e)
@@ -864,19 +888,20 @@ export default {
         this.elemType = extractedText.elemType
         this.elemDisc = extractedText.elemDisc
 
-        if (extractedText.ariaNode === this.focusedNode) {
-          console.log('已经由于(可能是点击导致的)focus而朗读了,不用再因为hover而朗读了');
-          return
-        } else {
-          this.planToPlayAudio()
-        }
+        this.planToPlayAudio()
       }
     }, 500),
     onMouseOverForContinueRead(e) {
       if (!this.ariaSettings.isCompActive) {
         return
       }
-      if (this.ignoreMouseOver) {
+      const curTime = Date.now()
+      if (curTime - this.domChangeLastTime <= 100) {
+        console.log('DOM刚改变,忽略hover。');
+        return
+      }
+      if (curTime - this.requestReadLastTime <= 100) {
+        console.log('刚被要求朗读,忽略hover。');
         return
       }
       const extractedText = utils.extractTextForMagnify(e)
@@ -918,18 +943,23 @@ export default {
       if (!this.ariaSettings.isCompActive) {
         return
       }
+      // 如果是点击鼠标引起的focus
+      if (this.isJustMouseDown) {
+        if (
+          document.activeElement.dataset.ariaNavigationArea !== undefined ||
+          document.activeElement.dataset.ariaViewportArea !== undefined ||
+          document.activeElement.dataset.ariaInteractionArea !== undefined
+        ) {
+          document.activeElement.blur()
+        }
+        console.log('刚按下鼠标,忽略focus。');
+        return
+      }
       const extractedText = utils.extractTextForMagnify(e, true)
       if (extractedText) {
         this.elemType = extractedText.elemType
         this.elemDisc = extractedText.elemDisc
 
-        this.focusedNode = extractedText.ariaNode
-        clearTimeout(this.focusedNodeTimeoutId)
-        this.focusedNodeTimeoutId = setTimeout(() => {
-          this.focusedNode = null
-          this.focusedNodeTimeoutId = null
-        }, 1000);
-
         this.planToPlayAudio(this.continueReadTaskId)
       }
     },
@@ -939,15 +969,15 @@ export default {
       this.ariaSettings = copy
     },
     onClickReset() {
-      this.planToPlayAudio('', "You've reset the feature settings")
+      this.$eventBus.$emit('request-read', "You've reset the feature settings")
       this.reset()
     },
     onClickMute() {
       this.ariaSettings.isMuted = !this.ariaSettings.isMuted
       if (this.ariaSettings.isMuted) {
-        // this.planToPlayAudio('', 'Sound off')
+        // this.$eventBus.$emit('request-read', "Sound off")
       } else {
-        this.planToPlayAudio('', 'Sound on')
+        this.$eventBus.$emit('request-read', "Sound on")
       }
       if (this.audioPlayer) {
         this.audioPlayer.muted = this.ariaSettings.isMuted
@@ -959,11 +989,11 @@ export default {
         this.ariaSettings.speechRateLevel = 0
       }
       if (this.ariaSettings.speechRateLevel === 0) {
-        this.planToPlayAudio('', 'Speak slowly')
+        this.$eventBus.$emit('request-read', "Speak slowly")
       } else if (this.ariaSettings.speechRateLevel === 1) {
-        this.planToPlayAudio('', 'Speak normally')
+        this.$eventBus.$emit('request-read', "Speak normally")
       } else if (this.ariaSettings.speechRateLevel === 2) {
-        this.planToPlayAudio('', 'Speak fast')
+        this.$eventBus.$emit('request-read', "Speak fast")
       }
       if (this.audioPlayer) {
         this.audioPlayer.playbackRate = speechRateFactors[this.ariaSettings.speechRateLevel]
@@ -971,10 +1001,10 @@ export default {
     },
     onClickScreenReaderMode() {
       if (this.ariaSettings.readMode === 'point') {
-        this.planToPlayAudio('', "You've enabled continuous reading mode.Please move the mouse over on the text you need to read,it'll start reading the screen in 1 second. When reading the link, tap the Enter key to enter the corresponding page.")
+        this.$eventBus.$emit('request-read', "You've enabled continuous reading mode.Please move the mouse over on the text you need to read,it'll start reading the screen in 1 second. When reading the link, tap the Enter key to enter the corresponding page.")
         this.ariaSettings.readMode = 'continue'
       } else if (this.ariaSettings.readMode === 'continue') {
-        this.planToPlayAudio('', "You've enabled read-only mode.Please move the mouse over on the text you need to read,Blind users can operate the keyboard directly.")
+        this.$eventBus.$emit('request-read', "You've enabled read-only mode.Please move the mouse over on the text you need to read,Blind users can operate the keyboard directly.")
         this.ariaSettings.readMode = 'point'
       }
     },
@@ -984,78 +1014,68 @@ export default {
         this.ariaSettings.themeIdx = 0
       }
       if (this.ariaSettings.themeIdx === 0) {
-        this.planToPlayAudio('', "Ajust to standard color.")
+        this.$eventBus.$emit('request-read', "Ajust to standard color.")
       } else if (this.ariaSettings.themeIdx === 1) {
-        this.planToPlayAudio('', "Adjust to black lettering on white background.")
+        this.$eventBus.$emit('request-read', "Adjust to black lettering on white background.")
       } else if (this.ariaSettings.themeIdx === 2) {
-        this.planToPlayAudio('', "Adjust to yellow lettering on blue background.")
+        this.$eventBus.$emit('request-read', "Adjust to yellow lettering on blue background.")
       } else if (this.ariaSettings.themeIdx === 3) {
-        this.planToPlayAudio('', "Adjust to black lettering on yellow background.")
+        this.$eventBus.$emit('request-read', "Adjust to black lettering on yellow background.")
       } else if (this.ariaSettings.themeIdx === 4) {
-        this.planToPlayAudio('', "Adjust to yellow lettering on black background.")
+        this.$eventBus.$emit('request-read', "Adjust to yellow lettering on black background.")
       }
     },
     onClickZoomIn() {
       if (this.ariaSettings.zoomLevel === zoomFactors.length - 1) {
         return
       }
-      this.planToPlayAudio('', "Zooming in on page")
+      this.$eventBus.$emit('request-read', "Zooming in on page")
       this.ariaSettings.zoomLevel++
     },
     onClickZoomOut() {
       if (this.ariaSettings.zoomLevel === 0) {
         return
       }
-      this.planToPlayAudio('', "Zooming out on page")
+      this.$eventBus.$emit('request-read', "Zooming out on page")
       this.ariaSettings.zoomLevel--
     },
     onClickCursorStyle() {
       this.ariaSettings.isBigCursor = !this.ariaSettings.isBigCursor
       if (this.ariaSettings.isBigCursor) {
-        this.planToPlayAudio('', "You've enabled the large cursor")
+        this.$eventBus.$emit('request-read', "You've enabled the large cursor")
       } else {
-        this.planToPlayAudio('', "You've disabled the large cursor")
+        this.$eventBus.$emit('request-read', "You've disabled the large cursor")
       }
     },
     onClickCrossCursor() {
       this.ariaSettings.isCursorCrosshair = !this.ariaSettings.isCursorCrosshair
       if (this.ariaSettings.isCursorCrosshair) {
-        this.planToPlayAudio('', "You've enabled the cross cursor")
+        this.$eventBus.$emit('request-read', "You've enabled the cross cursor")
       } else {
-        this.planToPlayAudio('', "You've disabled the cross cursor")
+        this.$eventBus.$emit('request-read', "You've disabled the cross cursor")
       }
     },
     onClickMagnifier() {
       this.ariaSettings.isMagnifying = !this.ariaSettings.isMagnifying
       if (this.ariaSettings.isMagnifying) {
-        this.planToPlayAudio('', "You've enabled the magnifier")
+        this.$eventBus.$emit('request-read', "You've enabled the magnifier")
       } else {
-        this.planToPlayAudio('', "You've disabled the magnifier")
+        this.$eventBus.$emit('request-read', "You've disabled the magnifier")
       }
     },
     onClickHelp() {
       window.open(config.publicPath + 'help.html')
     },
     onClickDownloadShortcut() {
-      this.planToPlayAudio('', "You are downloading the shortcut. Double click the shortcut to reach the website.")
+      this.$eventBus.$emit('request-read', "You are downloading the shortcut. Double click the shortcut to reach the website.")
     },
     onClickElderlyServicesAreaEntry() {
-      this.ignoreMouseOver = true
-      setTimeout(() => {
-        this.ignoreMouseOver = false
-      }, 1000);
-
       this.ariaSettings.menuMode = 'old'
-      this.planToPlayAudio('', "You've switched to the elderly services mode.")
+      this.$eventBus.$emit('request-read', "You've switched to the elderly services mode.")
     },
     onClickScreenReaderAreaEntry() {
-      this.ignoreMouseOver = true
-      setTimeout(() => {
-        this.ignoreMouseOver = false
-      }, 1000);
-
       this.ariaSettings.menuMode = 'blind'
-      this.planToPlayAudio('', "You've switched to screen the reading accessibility mode.")
+      this.$eventBus.$emit('request-read', "You've switched to screen the reading accessibility mode.")
     },
     onMouseDownNavigationArea(e) {
       e.preventDefault()