Имитация загрузки страницы 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">&copy; 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.