Skip to content

Commit 9d50e07

Browse files
Merge pull request #118 from SyncfusionExamples/ES-1006970-AccessibleDev
1006970: Resolved the iPad issues occurred in the Accessiblity sample
2 parents 1ca0ac2 + 6bd324b commit 9d50e07

2 files changed

Lines changed: 283 additions & 7 deletions

File tree

Accessible/EdgeScreenReader/wwwroot/accessibility.js

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,141 @@
1-

1+
const synth = window.speechSynthesis;
2+
3+
let voices = [];
4+
5+
const voicesReady = new Promise((resolve) => {
6+
const tryGet = () => {
7+
const voice = synth.getVoices();
8+
if (voice && voice.length) {
9+
voices = voice;
10+
resolve(voice);
11+
return true;
12+
}
13+
return false;
14+
};
15+
if (tryGet()) return;
16+
const onVoices = () => {
17+
if (tryGet()) {
18+
synth.removeEventListener('voiceschanged', onVoices);
19+
}
20+
};
21+
synth.addEventListener('voiceschanged', onVoices);
22+
// Fallback polling for browsers that don't fire voiceschanged reliably
23+
let tries = 0;
24+
const poll = setInterval(() => {
25+
if (tryGet() || ++tries > 30) {
26+
clearInterval(poll);
27+
voices = synth.getVoices() || [];
28+
synth.removeEventListener('voiceschanged', onVoices);
29+
resolve(voices);
30+
}
31+
}, 100);
32+
});
33+
34+
// iOS/iPadOS Safari requires a user gesture before speech works reliably.
35+
// This one-time unlock speaks a silent utterance on first tap/click/keydown.
36+
let __ttsUnlocked = false;
37+
let __unlockPromise = null;
38+
function ensureTtsUnlocked() {
39+
if (__ttsUnlocked) return Promise.resolve();
40+
if (__unlockPromise) return __unlockPromise;
41+
__unlockPromise = new Promise((resolve) => {
42+
const cleanup = () => {
43+
['click', 'touchstart', 'keydown'].forEach(evt => document.removeEventListener(evt, onEvent, true));
44+
};
45+
const onEvent = () => {
46+
try {
47+
const u = new SpeechSynthesisUtterance(''); // silent token
48+
u.volume = 0;
49+
u.rate = 1;
50+
u.onend = () => {
51+
__ttsUnlocked = true;
52+
cleanup();
53+
resolve();
54+
};
55+
// Queue in a macrotask to avoid race with gesture handling
56+
setTimeout(() => synth.speak(u), 0);
57+
} catch (_) {
58+
__ttsUnlocked = true;
59+
cleanup();
60+
resolve();
61+
}
62+
};
63+
['click', 'touchstart', 'keydown'].forEach(evt => document.addEventListener(evt, onEvent, true));
64+
});
65+
return __unlockPromise;
66+
}
67+
68+
function populateVoiceList() {
69+
voices = synth.getVoices().sort(function (a, b) {
70+
const aname = a.name.toUpperCase();
71+
const bname = b.name.toUpperCase();
72+
73+
if (aname < bname) {
74+
return -1;
75+
} else if (aname == bname) {
76+
return 0;
77+
} else {
78+
return +1;
79+
}
80+
});
81+
}
82+
83+
async function speakFromControls(input) {
84+
await voicesReady;
85+
86+
const t = (typeof input === 'string' ? input.trim() : (input?.value || '').trim());
87+
if (!t) return;
88+
89+
// Cancel any current speech to avoid overlaps/refresh quirks
90+
if (synth.speaking) {
91+
synth.cancel();
92+
}
93+
94+
const utterThis = new SpeechSynthesisUtterance(t);
95+
96+
utterThis.onend = function () {
97+
console.log("SpeechSynthesisUtterance.onend");
98+
};
99+
100+
const available = speechSynthesis.getVoices();
101+
let voice = null;
102+
voice = available.find(v => v.default) || available[0];
103+
if (voice) {
104+
utterThis.voice = voice;
105+
if (voice.lang) utterThis.lang = voice.lang; // Safari iOS respects lang better
106+
}
107+
108+
utterThis.pitch = 1;
109+
utterThis.rate = 1;
110+
111+
// Safari sometimes needs a microtask delay after cancel before speak
112+
setTimeout(() => synth.speak(utterThis), 0);
113+
}
114+
115+
async function initUi() {
116+
await voicesReady;
117+
// Populate voices now that controls exist
118+
populateVoiceList();
119+
if (speechSynthesis.onvoiceschanged !== undefined) {
120+
speechSynthesis.onvoiceschanged = populateVoiceList;
121+
}
122+
return true;
123+
}
124+
125+
// Initialize when DOM is ready; also handle Blazor re-renders
126+
document.addEventListener('DOMContentLoaded', async () => {
127+
// Set up iOS unlock listeners early
128+
ensureTtsUnlocked();
129+
if (await initUi()) return;
130+
const obs = new MutationObserver(async () => {
131+
if (await initUi()) obs.disconnect();
132+
});
133+
obs.observe(document.body, { childList: true, subtree: true });
134+
});
135+
136+
// Expose a helper to manually unlock from .NET or UI (tap/click)
137+
window.unlockTtsForIOS = () => ensureTtsUnlocked();
138+
2139
// Initialize PDF accessibility features and observe page changes
3140
function initPdfAccessibility() {
4141
const viewerInfo = getViewerInfo();
@@ -81,8 +218,7 @@ function wirePage(div) {
81218
// Reader the selected text aloud - Mircosoft Reader
82219
function readAloudText(text) {
83220
window.speechSynthesis.cancel();
84-
const utterance = new SpeechSynthesisUtterance(text);
85-
window.speechSynthesis.speak(utterance);
221+
speakFromControls(text);
86222
}
87223

88224
// Cancel speech and remove highlights - Mircosoft Reader

Accessible/WebSynthesis/wwwroot/accessibility.js

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,146 @@
1-
// Register .NET object for interop
1+
const synth = window.speechSynthesis;
2+
3+
let voices = [];
4+
5+
const voicesReady = new Promise((resolve) => {
6+
const tryGet = () => {
7+
const voice = synth.getVoices();
8+
if (voice && voice.length) {
9+
voices = voice;
10+
resolve(voice);
11+
return true;
12+
}
13+
return false;
14+
};
15+
if (tryGet()) return;
16+
const onVoices = () => {
17+
if (tryGet()) {
18+
synth.removeEventListener('voiceschanged', onVoices);
19+
}
20+
};
21+
synth.addEventListener('voiceschanged', onVoices);
22+
// Fallback polling for browsers that don't fire voiceschanged reliably
23+
let tries = 0;
24+
const poll = setInterval(() => {
25+
if (tryGet() || ++tries > 30) {
26+
clearInterval(poll);
27+
voices = synth.getVoices() || [];
28+
synth.removeEventListener('voiceschanged', onVoices);
29+
resolve(voices);
30+
}
31+
}, 100);
32+
});
33+
34+
// iOS/iPadOS Safari requires a user gesture before speech works reliably.
35+
// This one-time unlock speaks a silent utterance on first tap/click/keydown.
36+
let __ttsUnlocked = false;
37+
let __unlockPromise = null;
38+
function ensureTtsUnlocked() {
39+
if (__ttsUnlocked) return Promise.resolve();
40+
if (__unlockPromise) return __unlockPromise;
41+
__unlockPromise = new Promise((resolve) => {
42+
const cleanup = () => {
43+
['click', 'touchstart', 'keydown'].forEach(evt => document.removeEventListener(evt, onEvent, true));
44+
};
45+
const onEvent = () => {
46+
try {
47+
const u = new SpeechSynthesisUtterance(''); // silent token
48+
u.volume = 0;
49+
u.rate = 1;
50+
u.onend = () => {
51+
__ttsUnlocked = true;
52+
cleanup();
53+
resolve();
54+
};
55+
// Queue in a macrotask to avoid race with gesture handling
56+
setTimeout(() => synth.speak(u), 0);
57+
} catch (_) {
58+
__ttsUnlocked = true;
59+
cleanup();
60+
resolve();
61+
}
62+
};
63+
['click', 'touchstart', 'keydown'].forEach(evt => document.addEventListener(evt, onEvent, true));
64+
});
65+
return __unlockPromise;
66+
}
67+
68+
function populateVoiceList() {
69+
voices = synth.getVoices().sort(function (a, b) {
70+
const aname = a.name.toUpperCase();
71+
const bname = b.name.toUpperCase();
72+
73+
if (aname < bname) {
74+
return -1;
75+
} else if (aname == bname) {
76+
return 0;
77+
} else {
78+
return +1;
79+
}
80+
});
81+
}
82+
83+
async function speakFromControls(input) {
84+
// iOS/iPadOS: require user-gesture unlock and stable voice list
85+
await voicesReady;
86+
87+
const t = (typeof input === 'string' ? input.trim() : (input?.value || '').trim());
88+
if (!t) return;
89+
90+
// Cancel any current speech to avoid overlaps/refresh quirks
91+
if (synth.speaking) {
92+
synth.cancel();
93+
}
94+
95+
const utterThis = new SpeechSynthesisUtterance(t);
96+
97+
utterThis.onend = function () {
98+
console.log("SpeechSynthesisUtterance.onend");
99+
};
100+
101+
const available = speechSynthesis.getVoices();
102+
let voice = null;
103+
voice = available.find(v => v.default) || available[0];
104+
if (voice) {
105+
utterThis.voice = voice;
106+
if (voice.lang) utterThis.lang = voice.lang; // Safari iOS respects lang better
107+
}
108+
109+
utterThis.pitch = 1;
110+
utterThis.rate = 1;
111+
112+
// Safari sometimes needs a microtask delay after cancel before speak
113+
setTimeout(() => synth.speak(utterThis), 0);
114+
}
115+
116+
async function initUi() {
117+
await voicesReady;
118+
// Populate voices now that controls exist
119+
populateVoiceList();
120+
if (speechSynthesis.onvoiceschanged !== undefined) {
121+
speechSynthesis.onvoiceschanged = populateVoiceList;
122+
}
123+
return true;
124+
}
125+
126+
// Initialize when DOM is ready; also handle Blazor re-renders
127+
document.addEventListener('DOMContentLoaded', async () => {
128+
// Set up iOS unlock listeners early
129+
ensureTtsUnlocked();
130+
if (await initUi()) return;
131+
const obs = new MutationObserver(async () => {
132+
if (await initUi()) obs.disconnect();
133+
});
134+
obs.observe(document.body, { childList: true, subtree: true });
135+
});
136+
137+
// Expose a helper to manually unlock from .NET or UI (tap/click)
138+
window.unlockTtsForIOS = () => ensureTtsUnlocked();
139+
140+
// Register .NET object for interop
2141
function registerDotNetObject(dotNetObj) {
3-
window.myDotNetObj = dotNetObj;
142+
window.myDotNetObj = dotNetObj;
143+
ensureTtsUnlocked();
4144
}
5145
// Read selected text and highlight
6146
function readSelectedText(args, zoomLevel, muteVoice) {
@@ -15,7 +155,7 @@ function readSelectedText(args, zoomLevel, muteVoice) {
15155
});
16156
if (muteVoice) return;
17157
requestAnimationFrame(() => {
18-
speakText(text, clearAllHighlights);
158+
speakFromControls(text);
19159
});
20160
}
21161
// Read a line from page and notify .NET
@@ -47,7 +187,7 @@ function readLineFromPage(pageIndex, lineIndex, isPrev, muteVoice) {
47187
if (currentLineSpans && !muteVoice) {
48188
const lineText = currentLineSpans.map(s => s.textContent).join(' ');
49189
requestAnimationFrame(() => {
50-
speakText(lineText, clearAllHighlights);
190+
speakFromControls(lineText);
51191
});
52192
}
53193
}

0 commit comments

Comments
 (0)