Имитация загрузки страницы Voiceover в одностраничном веб-приложении pushState
Я работаю над одностраничным приложением (SPA), где мы моделируем многостраничное приложение с HTML 5 history.pushState
. Он отлично выглядит визуально, но он не корректно работает в iOS Voiceover. (Я предполагаю, что это не сработает ни в одном экране, но Voiceover - это то, что я пытаюсь сделать первым.)
Вот пример поведения, которого я пытаюсь достичь. Вот две обычные веб-страницы:
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>
Приятный и простой. Voiceover читает это следующим образом:
Загружена веб-страница. Это страница 1.
[проведите пальцем вправо] Нажмите здесь, чтобы перейти на страницу 2. Ссылка.
[двойное нажатие] Загружена веб-страница. Это страница 2.
[проведите пальцем вправо] Нажмите здесь, чтобы перейти на страницу 1. Посетили. Link.
[двойное нажатие] Загружена веб-страница. Это страница 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 "загружается быстрее" (потому что она совсем не загружается, все это происходит в JS).
Но в Voiceover это не так.
Загружена веб-страница. Это страница 1.
[проведите пальцем вправо] Нажмите здесь, чтобы перейти на страницу 2. Ссылка.
[double tap] Нажмите здесь, чтобы перейти на страницу 1. Посетили. Link.
[Основное внимание уделяется ссылке! проведите пальцем влево] Это страница 2.
[проведите пальцем вправо] Нажмите здесь, чтобы перейти на страницу 1. Посетили. Link.
[двойное нажатие] Загружена веб-страница. Это страница 1.
Основное внимание уделяется ссылке, когда она должна быть в верхней части страницы.
Как сообщить Voiceover, что вся страница только что обновлена, и поэтому читатель должен возобновить чтение в верхней части страницы?
Ответы
Ответ 1
Jorgen answer, основанный на fooobar.com/questions/503126/..., дал мне правильный путь.
Фактическое исправление не состояло в том, чтобы обернуть всю страницу в <div tabindex=-1>
, а вместо этого создать <span tabindex=-1>
только в первой части ( "Это страница N" ) и сфокусировать это.
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();
}
Примечание. В этом примере мы также размываем фокус в тайм-ауте; в противном случае браузеры без экрана будут рисовать видимый синий прямоугольник фокуса вокруг пролета, чего мы не хотим. Размытие фокуса не влияет на фокус чтения iOS VO.
Ответ 2
У меня нет iPhone, но решение предоставляется в другом потоке fooobar.com/questions/503126/.... Оберните содержимое в элементе с 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();
}
Ответ 3
Здесь дальнейшее улучшение в ответ на ответ Дэн Фабулих.
Вопрос состоял в обновлении страницы с помощью 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 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 no-no.
- Менее плохо, чем размытие CSS
span {outline:none}
, но это рискованно. Я бы использовал его только в статическом тексте с tabindex=-1
, и он не идеален, потому что он дезориентирует внимание на знакомых пользователей клавиатуры.
- Лучшее решение, которое я видел, это Простой вариант ввода Alice Boxhall.