Skip to content

Commit 0c757f8

Browse files
author
Chris Lorenzo
committed
update announcer to be classless
1 parent 0c800bf commit 0c757f8

File tree

1 file changed

+88
-140
lines changed

1 file changed

+88
-140
lines changed

src/Accessibility/Announcer.js

Lines changed: 88 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -17,168 +17,116 @@
1717
* limitations under the License.
1818
*/
1919

20-
import Speech from './Speech.js'
20+
import SpeechEngine from './Speech.js'
2121
import { debounce, getElmName } from './utils.js'
2222

23-
const defaultOptions = {
24-
voiceOutDelay: 500,
25-
announcerFocusDebounce: 400,
26-
announcerTimeout: 5 * 60 * 1000, //Five Minutes
27-
}
28-
29-
export default function Announcer(Base, speak = Speech, announcerOptions = {}) {
30-
const options = { ...defaultOptions, ...announcerOptions }
31-
32-
return class extends Base {
33-
_construct() {
34-
this._debounceAnnounceFocusChanges = debounce(
35-
this._announceFocusChanges.bind(this),
36-
options.announcerFocusDebounce
37-
)
23+
let resetFocusTimer;
24+
let prevFocusPath = [];
25+
let currentlySpeaking;
26+
let voiceOutDisabled = false;
27+
const fiveMinutes = 300000;
3828

39-
this._resetFocusTimer = debounce(() => {
40-
// Reset focus path for full announce
41-
this._lastFocusPath = undefined
42-
}, options.announcerTimeout)
43-
44-
// Lightning only calls Focus Change on second focus
45-
this._focusChange()
46-
}
47-
48-
_voiceOut(toSpeak) {
49-
if (this._voiceOutDisabled) {
50-
return
51-
}
29+
function onFocusChangeCore(focusPath = []) {
30+
if (!Announcer.enabled) {
31+
return
32+
}
5233

53-
const speech = speak(toSpeak)
54-
// event using speech synthesis api promise
55-
if (speech && speech.series) {
56-
speech.series.then(() => {
57-
this.stage.emit('announceEnded')
58-
})
59-
}
34+
const loaded = focusPath.every(elm => !elm.loading)
35+
const focusDiff = focusPath.filter(elm => !prevFocusPath.includes(elm))
6036

61-
// event in case speech synthesis api is flakey,
62-
// assume the ammount of time it takes to read each word
63-
const toAnnounceStr = Array.isArray(toSpeak) ? toSpeak.concat().join(' ') : toSpeak
64-
const toAnnounceWords = toAnnounceStr.split(' ')
65-
const timeoutDelay = toAnnounceWords.length * options.voiceOutDelay
66-
clearTimeout(this._announceEndedTimeout)
67-
this._announceEndedTimeout = setTimeout(() => {
68-
this.stage.emit('announceTimeoutEnded')
69-
}, timeoutDelay)
70-
71-
return speech
72-
}
37+
resetFocusTimer()
38+
Announcer.cancel()
7339

74-
_disable() {
75-
clearTimeout(this._announceEndedTimeout)
76-
this.stage.emit('announceEnded')
77-
this.stage.emit('announceTimeoutEnded')
78-
}
40+
if (!loaded) {
41+
Announcer.onFocusChange()
42+
return
43+
}
7944

80-
set announcerEnabled(val) {
81-
this._announcerEnabled = val
82-
this._focusChange()
83-
}
45+
prevFocusPath = focusPath.slice(0)
8446

85-
get announcerEnabled() {
86-
return this._announcerEnabled
47+
let toAnnounceText = [];
48+
let toAnnounce = focusDiff.reduce((acc, elm) => {
49+
if (elm.announce) {
50+
acc.push([getElmName(elm), 'Announce', elm.announce])
51+
toAnnounceText.push(elm.announce)
52+
} else if (elm.title) {
53+
acc.push([getElmName(elm), 'Title', elm.title])
54+
toAnnounceText.push(elm.title)
8755
}
88-
89-
_focusChange() {
90-
if (!this._resetFocusTimer) {
91-
return
92-
}
93-
94-
this._resetFocusTimer()
95-
this.$announcerCancel()
96-
this._debounceAnnounceFocusChanges()
56+
return acc
57+
}, [])
58+
59+
focusDiff.reverse().reduce((acc, elm) => {
60+
if (elm.announceContext) {
61+
acc.push([getElmName(elm), 'Context', elm.announceContext])
62+
toAnnounceText.push(elm.announceContext)
63+
} else {
64+
acc.push([getElmName(elm), 'No Context', ''])
9765
}
66+
return acc
67+
}, toAnnounce)
9868

99-
_announceFocusChanges() {
100-
const focusPath = this.application.focusPath || []
101-
const lastFocusPath = this._lastFocusPath || []
102-
const loaded = focusPath.every(elm => !elm.loading)
103-
const focusDiff = focusPath.filter(elm => !lastFocusPath.includes(elm))
104-
105-
if (!loaded) {
106-
this._debounceAnnounceFocusChanges()
107-
return
108-
}
109-
110-
this._lastFocusPath = focusPath.slice(0)
111-
// Provide hook for focus diff for things like TextBanner
112-
this.focusDiffHook = focusDiff
113-
114-
if (!this.announcerEnabled) {
115-
return
116-
}
117-
118-
let toAnnounce = focusDiff.reduce((acc, elm) => {
119-
if (elm.announce) {
120-
acc.push([getElmName(elm), 'Announce', elm.announce])
121-
} else if (elm.title) {
122-
acc.push([getElmName(elm), 'Title', elm.title || ''])
123-
}
124-
return acc
125-
}, [])
126-
127-
focusDiff.reverse().reduce((acc, elm) => {
128-
if (elm.announceContext) {
129-
acc.push([getElmName(elm), 'Context', elm.announceContext])
130-
} else {
131-
acc.push([getElmName(elm), 'No Context', ''])
132-
}
133-
return acc
134-
}, toAnnounce)
135-
136-
if (this.debug) {
137-
console.table(toAnnounce)
138-
}
69+
if (Announcer.debug) {
70+
console.table(toAnnounce)
71+
}
13972

140-
toAnnounce = toAnnounce.reduce((acc, a) => {
141-
const txt = a[2]
142-
txt && acc.push(txt)
143-
return acc
144-
}, [])
145-
146-
if (toAnnounce.length) {
147-
this.$announcerCancel()
148-
this._currentlySpeaking = this._voiceOut(
149-
toAnnounce.reduce((acc, val) => acc.concat(val), [])
150-
)
151-
}
152-
}
73+
if (toAnnounceText.length) {
74+
Announcer.cancel()
75+
return currentlySpeaking = Announcer._textToSpeech(
76+
toAnnounceText.reduce((acc, val) => acc.concat(val), [])
77+
)
78+
}
79+
}
15380

154-
$announce(toAnnounce, { append = false, notification = false } = {}) {
155-
if (this.announcerEnabled) {
156-
this._debounceAnnounceFocusChanges.flush()
157-
if (append && this._currentlySpeaking && this._currentlySpeaking.active) {
158-
this._currentlySpeaking.append(toAnnounce)
81+
const Announcer = {
82+
enabled: true,
83+
debug: false,
84+
cancel: function() {
85+
currentlySpeaking && currentlySpeaking.cancel()
86+
},
87+
refresh: function(depth = 0) {
88+
prevFocusPath = prevFocusPath.slice(0, depth)
89+
resetFocusTimer()
90+
},
91+
speak: function(text, { append = false, notification = false } = {}) {
92+
if (Announcer.enabled) {
93+
Announcer.onFocusChange.flush()
94+
if (append && currentlySpeaking && currentlySpeaking.active) {
95+
currentlySpeaking.append(text)
15996
} else {
160-
this.$announcerCancel()
161-
this._currentlySpeaking = this._voiceOut(toAnnounce)
97+
Announcer.cancel()
98+
currentlySpeaking = Announcer._textToSpeech(text)
16299
}
163100

164101
if (notification) {
165-
this._voiceOutDisabled = true
166-
this._currentlySpeaking.series.finally(() => {
167-
this._voiceOutDisabled = false
168-
this.$announcerRefresh()
102+
voiceOutDisabled = true
103+
currentlySpeaking.series.finally(() => {
104+
voiceOutDisabled = false
105+
Announcer.refresh()
169106
})
170107
}
171108
}
172-
}
173109

174-
$announcerCancel() {
175-
this._currentlySpeaking && this._currentlySpeaking.cancel()
110+
return currentlySpeaking
111+
},
112+
_textToSpeech(toSpeak) {
113+
if (voiceOutDisabled) {
114+
return
176115
}
177116

178-
$announcerRefresh(depth = 0) {
179-
this._lastFocusPath = this._lastFocusPath.slice(0, depth)
180-
this._resetFocusTimer()
181-
this._focusChange()
182-
}
117+
return SpeechEngine(toSpeak)
118+
},
119+
init: function(focusDebounce = 400, focusChangeTimeout = fiveMinutes) {
120+
Announcer.onFocusChange = debounce(
121+
onFocusChangeCore,
122+
focusDebounce
123+
);
124+
125+
resetFocusTimer = debounce(() => {
126+
// Reset focus path for full announce
127+
prevFocusPath = []
128+
}, focusChangeTimeout)
183129
}
184130
}
131+
Announcer.init();
132+
export default Announcer;

0 commit comments

Comments
 (0)