在单页 pushState Web 应用程序中模拟画外音页面加载
Simulate Voiceover page load in single-page pushState web application
我正在开发单页应用程序 (SPA),我们正在使用 HTML 5 history.pushState
模拟多页应用程序。它在视觉上看起来不错,但在 iOS 画外音中表现不正确。 (我假设它在任何屏幕上都不起作用 reader,但画外音是我首先尝试的。)
这是我试图实现的行为示例。下面是两个普通网页:
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>
漂亮又简单。画外音是这样读的:
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.
这里又是一个单页应用程序,使用历史操作来模拟实际页面加载。
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();
};
(请注意,此示例不适用于文件系统;您必须从网络服务器加载它。)
这个 SPA 示例在视觉上看起来与简单的多页示例完全一样,除了第 2 页 "loads quicker"(因为它根本没有真正加载;它全部发生在 JS 中)。
但在 Voiceover 中,它并没有做正确的事情。
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.
焦点在 link 上,而它本应位于页面顶部。
如何告诉 Voiceover 整个页面刚刚更新,因此 reader 应该从页面顶部继续阅读?
我没有 iPhone,但另一个 Whosebug thread 中提供了解决方案。将内容包装在 tabindex 为 -1 的元素中,并以编程方式将焦点设置在该元素上。
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 = '<div tabindex="-1" id="page' + page + '">This is page ' + page +
'.\n<a href='spa'+otherPage+'.html'>Click here for page ' +
otherPage + '.</a></div>';
document.getElementById('page' + page).focus();
attachHandler();
}
, which is based on another Whosebug thread 让我走上了正确的轨道。
实际的解决方法不是将整个页面包裹在 <div tabindex=-1>
中,而是在第一部分 ("This is page N") 周围创建一个 <span tabindex=-1>
并重点关注该部分。
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 = '<span tabindex="-1" id="page' + page + '">This is page ' + page +
'.</span>\n<a href="spa'+otherPage+'.html">Click here for page ' +
otherPage + '.</a>';
document.getElementById('page' + page).focus();
setTimeout(function() {
document.getElementById('page' + page).blur();
}, 0);
attachHandler();
}
请注意,在这个例子中,我们也在超时中模糊了焦点;否则,非屏幕-reader 浏览器将在跨度周围绘制一个可见的蓝色焦点矩形,这不是我们想要的。模糊焦点不会影响 iOS VO reader.
的焦点
这里有进一步的改进,以响应 。
问题是关于使用 pushState()
更新页面,所以我将从包含要更新的主要内容区域的语义 HTML 结构开始。
<body>
<header role="banner">
<nav role="navigation">
<a href="page1.html" onclick="updatePage(1, 'Page 1', this.href); return false;" aria-current="true">Page 1</a> |
<a href="page2.html" onclick="updatePage(2, 'Page 2', this.href); return false;">Page 2</a>
</nav>
</header>
<main role="main">
<!-- This heading text is immediately after the nav, so it's a logical place to set focus. -->
<h1 id="maintitle" tabindex="-1">Page 1</h1>
<div id="maincontent">
<p>Lorem ipsum</p>
</div>
</main>
<footer role="contentinfo">© 2017</footer>
</body>
JavaScript:
function updatePage(pageNumber, pageTitle, pagePath) {
var mainContent;
if (pageNumber == 1) {
mainContent = '<p>Lorem ipsum</p>';
} else if (pageNumber == 2) {
mainContent = '<p>Dolor sit amet</p>';
} else {
return;
}
document.getElementById('maincontent').innerHTML = mainContent;
// TODO: Update the address bar with pushState()
// TODO: Update the nav links, so only one link has aria-current="true"
// Keep the browser title bar in sync with the content.
document.title = pageTitle + ' - My Example Site';
// Set focus on the visible heading, so screen readers will announce it.
var mainTitleElement = document.getElementById('maintitle');
mainTitleElement.innerHTML = pageTitle;
mainTitleElement.focus(); // TODO: Add a 0ms timer, to ensure the DOM is ready.
}
关于对焦轮廓(也称为对焦环):
- 不要
blur()
。屏幕阅读器通常可以恢复,但不能重新播报当前行,这是 WCAG 的禁忌。
- CSS
span {outline:none}
比模糊更糟糕,但这是有风险的。我只会在带有 tabindex=-1
的静态文本上使用它,它并不理想,因为它会让视力正常的键盘用户迷失方向。
- 我见过的最好的解决方案是 Alice Boxhall's input modality prollyfill。
我正在开发单页应用程序 (SPA),我们正在使用 HTML 5 history.pushState
模拟多页应用程序。它在视觉上看起来不错,但在 iOS 画外音中表现不正确。 (我假设它在任何屏幕上都不起作用 reader,但画外音是我首先尝试的。)
这是我试图实现的行为示例。下面是两个普通网页:
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>
漂亮又简单。画外音是这样读的:
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.
这里又是一个单页应用程序,使用历史操作来模拟实际页面加载。
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();
};
(请注意,此示例不适用于文件系统;您必须从网络服务器加载它。)
这个 SPA 示例在视觉上看起来与简单的多页示例完全一样,除了第 2 页 "loads quicker"(因为它根本没有真正加载;它全部发生在 JS 中)。
但在 Voiceover 中,它并没有做正确的事情。
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.
焦点在 link 上,而它本应位于页面顶部。
如何告诉 Voiceover 整个页面刚刚更新,因此 reader 应该从页面顶部继续阅读?
我没有 iPhone,但另一个 Whosebug thread 中提供了解决方案。将内容包装在 tabindex 为 -1 的元素中,并以编程方式将焦点设置在该元素上。
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 = '<div tabindex="-1" id="page' + page + '">This is page ' + page +
'.\n<a href='spa'+otherPage+'.html'>Click here for page ' +
otherPage + '.</a></div>';
document.getElementById('page' + page).focus();
attachHandler();
}
实际的解决方法不是将整个页面包裹在 <div tabindex=-1>
中,而是在第一部分 ("This is page N") 周围创建一个 <span tabindex=-1>
并重点关注该部分。
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 = '<span tabindex="-1" id="page' + page + '">This is page ' + page +
'.</span>\n<a href="spa'+otherPage+'.html">Click here for page ' +
otherPage + '.</a>';
document.getElementById('page' + page).focus();
setTimeout(function() {
document.getElementById('page' + page).blur();
}, 0);
attachHandler();
}
请注意,在这个例子中,我们也在超时中模糊了焦点;否则,非屏幕-reader 浏览器将在跨度周围绘制一个可见的蓝色焦点矩形,这不是我们想要的。模糊焦点不会影响 iOS VO reader.
的焦点这里有进一步的改进,以响应
问题是关于使用 pushState()
更新页面,所以我将从包含要更新的主要内容区域的语义 HTML 结构开始。
<body>
<header role="banner">
<nav role="navigation">
<a href="page1.html" onclick="updatePage(1, 'Page 1', this.href); return false;" aria-current="true">Page 1</a> |
<a href="page2.html" onclick="updatePage(2, 'Page 2', this.href); return false;">Page 2</a>
</nav>
</header>
<main role="main">
<!-- This heading text is immediately after the nav, so it's a logical place to set focus. -->
<h1 id="maintitle" tabindex="-1">Page 1</h1>
<div id="maincontent">
<p>Lorem ipsum</p>
</div>
</main>
<footer role="contentinfo">© 2017</footer>
</body>
JavaScript:
function updatePage(pageNumber, pageTitle, pagePath) {
var mainContent;
if (pageNumber == 1) {
mainContent = '<p>Lorem ipsum</p>';
} else if (pageNumber == 2) {
mainContent = '<p>Dolor sit amet</p>';
} else {
return;
}
document.getElementById('maincontent').innerHTML = mainContent;
// TODO: Update the address bar with pushState()
// TODO: Update the nav links, so only one link has aria-current="true"
// Keep the browser title bar in sync with the content.
document.title = pageTitle + ' - My Example Site';
// Set focus on the visible heading, so screen readers will announce it.
var mainTitleElement = document.getElementById('maintitle');
mainTitleElement.innerHTML = pageTitle;
mainTitleElement.focus(); // TODO: Add a 0ms timer, to ensure the DOM is ready.
}
关于对焦轮廓(也称为对焦环):
- 不要
blur()
。屏幕阅读器通常可以恢复,但不能重新播报当前行,这是 WCAG 的禁忌。 - CSS
span {outline:none}
比模糊更糟糕,但这是有风险的。我只会在带有tabindex=-1
的静态文本上使用它,它并不理想,因为它会让视力正常的键盘用户迷失方向。 - 我见过的最好的解决方案是 Alice Boxhall's input modality prollyfill。