I'm working on a single-page application (SPA) where we're simulating a multi-page application with HTML 5 history.pushState
. It looks fine visually, but it's not behaving correctly in iOS Voiceover. (I assume it wouldn't work in any screen reader, but Voiceover is what I'm trying first.)
Here's an example of the behavior I'm trying to achieve. Here are two ordinary web pages:
1.html
<!DOCTYPE html><html>
<body>This is page 1. <a href=2.html>Click here for page 2.</a></body>
</html>
2.html
<!DOCTYPE html><html>
<body>This is page 2. <a href=1.html>Click here for page 1.</a></body>
</html>
Nice and simple. Voiceover reads it like this:
Web page loaded. This is page 1.
[swipe right] Click here for page 2. Link.
[double tap] Web page loaded. This is page 2.
[swipe right] Click here for page 1. Visited. Link.
[double tap] Web page loaded. This is page 1.
Here it is again as a single-page application, using history manipulation to simulate actual page loads.
spa1.html
<!DOCTYPE html><html>
<body>This is page 1.
<a href='spa2.html'>Click here for page 2.</a></body>
<script src="switchPage.js"></script>
</html>
spa2.html
<!DOCTYPE html><html>
<body>This is page 2.
<a href='spa1.html'>Click here for page 1.</a></body>
<script src="switchPage.js"></script>
</html>
switchPage.js
console.log("load");
attachHandler();
function attachHandler() {
document.querySelector('a').onclick = function(event) {
event.preventDefault();
history.pushState(null, null, this.href);
drawPage();
}
}
function drawPage() {
var page, otherPage;
// in real life, we'd do an AJAX call here or something
if (/spa1.html$/.test(window.location.pathname)) {
page = 1;
otherPage = 2;
} else {
page = 2;
otherPage = 1;
}
document.body.innerHTML = "This is page " + page +
".\n<a href='spa"+otherPage+".html'>Click here for page " +
otherPage + ".</a>";
attachHandler();
}
window.onpopstate = function() {
drawPage();
};
(Note that this sample doesn't work from the filesystem; you have to load it from a webserver.)
This SPA example visually looks exactly like the simple multi-page example, except that page 2 "loads quicker" (because it's not really loading at all; it's all happening in JS).
But in Voiceover, it doesn't do the right thing.
Web page loaded. This is page 1.
[swipe right] Click here for page 2. Link.
[double tap] Click here for page 1. Visited. Link.
[The focus is on the link! swipe left] This is page 2.
[swipe right] Click here for page 1. Visited. Link.
[double tap] Web page loaded. This is page 1.
The focus is on the link, when it should be at the top of the page.
How do I tell Voiceover that the whole page has just updated and so the reader should resume reading from the top of the page?