Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reader: screen reader cursor remaining in place after scrolling to another page #4515

Open
abaevbog opened this issue Aug 7, 2024 · 16 comments · May be fixed by zotero/reader#137
Open

Reader: screen reader cursor remaining in place after scrolling to another page #4515

abaevbog opened this issue Aug 7, 2024 · 16 comments · May be fixed by zotero/reader#137

Comments

@abaevbog
Copy link
Contributor

abaevbog commented Aug 7, 2024

https://forums.zotero.org/discussion/comment/469784/#Comment_469784

As described in the forums post, one issue is that screen readers may block keypresses used to switch between pages. This can be worked around by temporarily passing through a keystroke or switching from reading to focus mode for a moment. The bigger issue is that the cursor of screen readers remains unaffected by scrolling another page into view. This is understandable, since as long as the DOM does not change, NVDA has no reason to think that the cursor needs to move. It means that even if the page is scrolled, arrowDown will read the \ sentence after the last sentence on the previous page and (sometimes) scroll back to it, essentially undoing previous scroll.

As a workaround, I am thinking maybe we could exploit Control-Home/End keypresses of NVDA. Their purpose is to move the virtual cursor to the top/bottom of the page. It currently doesn't work right at all in the reader due to pages being dynamically loaded/unloaded, so it moves cursor to the first/last loaded page. What if we mark all pages but the previous and next one as aria-hidden, in which case Control-Home/End theoretically should only move the cursor to the previous/next page (since remaining ones are hidden). I'll try to see if it works.

@abaevbog
Copy link
Contributor Author

@mrtcode I have been looking into one of the accessibility issues with the reader described in this nice and detailed forums post and wanted to check in on what you think about it.

To sum up the issue, if you navigate the content of the PDF or EPUB reader via screen reader, the existing commands for page navigation get somewhat undone by the screen reader's functionality. If you use any of our shortcuts to scroll to the next page B from A, the virtual cursor of the screen reader will have no reason to leave page A. It'll just stay at the previous DOM element, so on the next arrowUp/Down, the screen reader will just read the next line on page A, potentially even scrolling back. That means that, as described in the forums post, if you rely on screen reader for navigating the reader, there is no functional way to traverse PDF and EPUB documents.

I looked into a few different ways to address it on an example of a PDF (which is easier), and the easiest but also most functional solution that I could come up with so far is to add a hidden header <h6> node as a child of the <div class="page"> nodes with the label or index of the page as the content. Then, JAWS and NVDA users could just use the same shortcuts as with normal web pages (or snapshots) to go to the next/previous heading (which would be equivalent to going to next/previous page in this case). The added benefit is that it would allow to easily identify which page you are on since that is also not always obvious right now, as described in the forums post as well.

Does it sound like a viable approach for the PDF overall? It gets a bit trickier with EPUB since it doesn't look like there are clear pages but rather the entire chapter rendered as a series of <p> nodes. But maybe we could insert the hidden headers between the paragraphs to serve the same purpose? Let me know what you think!

@mrtcode
Copy link
Member

mrtcode commented Aug 28, 2024

From the PDF view perspective, that sounds right. However, I'm not sure about the EPUB part. Maybe @AbeJellinek has some thoughts on this.

@abaevbog
Copy link
Contributor Author

Gotcha, thanks!

@abaevbog
Copy link
Contributor Author

I'm now realizing that while having such headers is helpful, for PDF it is not strictly needed since NVDA and JAWS do allow you to navigate between region components (which are pages) via their shortcuts. It is still necessary for EPUB though, since page numbers don't exist anywhere in the rendered content. I have a somewhat working setup, which in my opinion, would already be an improvement.

But the other thing for me to note is that we just need to have a way to force screen readers to move their virtual cursor to where we need. Otherwise, so many features are way harder to use. Manually changing the page number, selecting a section in the outline, using "Find in document" - all of these scroll to a particular spot in the document without the virtual cursor properly adjusting. So I am now trying out the approach of making an element in the document focusable after navigation, focusing it and them cleaning up after blur, which does seem to help.

It's a bit trickier and messier but this could be a more fundamental way to help screen readers.

@AbeJellinek
Copy link
Member

But maybe we could insert the hidden headers between the paragraphs to serve the same purpose?

We can't add/remove anything within <replaced-body> in the EPUB DOM, because EPUB CFIs (locations) work by counting child nodes. We can modify ARIA attributes on existing elements or make them focusable by adding a class.

(Some EPUBs do have hidden page number elements, but we can't count on that.)

@abaevbog
Copy link
Contributor Author

abaevbog commented Sep 3, 2024

We can't add/remove anything within in the EPUB DOM

Oh, that is good to know! It is what I've been going after... Hm, if we were to place a hidden <nav> element as a child of a paragraph from page mappings, instead of placing it between <p> elements, would it still cause issues?

@AbeJellinek
Copy link
Member

Yes.

@AbeJellinek
Copy link
Member

Unfortunately!

Is there no way to achieve the same thing by modifying the attributes of an existing element?

@abaevbog
Copy link
Contributor Author

abaevbog commented Sep 3, 2024

Yes.

Gotcha ... No DOM manipulation for epub content.

Is there no way to achieve the same thing by modifying the attributes of an existing element?

This is what I'll try next! I am trying to make the same interface for epub screen readers navigation as with pdfs (where each page is a "region", so d keypress takes you to the next page, and Shift-d takes you to the previous one). With <nav> node added as a sibling of paragraphs, it worked well because d takes you to the next region or landmark (which is what nav is). I'll see what would happen if we were to give role="navigation" to a paragraph. I don't think it's very correct semantically but maybe with a bit of extra aria-labels, we can make it happen!

@AbeJellinek
Copy link
Member

I mean, if the screen reader can navigate between <region>s in a PDF, why can't we set role="region" on the first element following the start of a location in the EPUB and achieve the same thing?

@AbeJellinek
Copy link
Member

AbeJellinek commented Sep 3, 2024

Or navigation!

(Note that EPUB documents are just HTML documents - there aren't necessarily top-level <p>s, a location can start in the middle of a word, one <p> or <div> or whatever could contain multiple location boundaries, ... But I think we'll be able to make 99% of books work well.)

@abaevbog
Copy link
Contributor Author

abaevbog commented Sep 3, 2024

I mean, if the screen reader can navigate between s in a PDF, why can't we set role="region" on the first element following the start of a location in the EPUB and achieve the same thing?

I think it is not quite correct semantically, since, for example, <p> has implicit role="paragraph" and we override it with something completely different. But the good news is that both NVDA and JAWS don't seem to mind and adding role="navigation" with aria-label="Page X" to the page mapping element still allows us to move between pages, and announce the label of the page, and still have the text accessible. So it's not quite correct but it does work! At least with the book I am testing on.

A question about EPUB locations, just to be sure: is it ok to insert a node if it's going to be shortly deleted? On Tab into the EPUB content after navigating the outline, the virtual cursor has no idea where to go (since it looks at rendered DOM disregarding the scroll position), so I insert and focus an invisible div that is removed on blur to force the screen reader to focus the selected section, instead of jumping to the start of the document.

@AbeJellinek
Copy link
Member

AbeJellinek commented Sep 3, 2024

It would be safe as long as we add and remove it synchronously; e.g. does something like this work?

handleFocus() {
	let firstVisibleElement = closestElement(this.flow.startRange.startContainer)!;
	let child = this._iframeDocument.createElement('div');
	firstVisibleElement.prepend(child);
	child.tabIndex = '0'; // or whatever we need
	child.focus();
	child.remove(); // or setTimeout(() => child.remove()), but that isn't as good
}

No way to do this by giving an existing element a tabIndex?

@abaevbog
Copy link
Contributor Author

abaevbog commented Sep 3, 2024

My current approach is this:

placeA11yVirtualCursor () {
  let target = this._state.ariaNodeTarget;
  let doc = this._lastView._iframe.contentDocument;
  if (!target || !doc.contains(target)) return;
  let dummy = doc.createElement("div");;
  dummy.id = "ariaFocusDummy";
  console.log("Virt cursor? ");
  dummy.setAttribute("tabindex", "-1");
  dummy.setAttribute("aria-label", "Focus Cursor");
  // Make it invisible
  dummy.style.clipPath = "inset(50%)";
  // On blur or keypress, remove it
  dummy.addEventListener("blur", (event) => {
	  event.target.remove();
  });
  dummy.addEventListener("keydown", (event) => {
	  event.target.blur();
  });
  // Insert into the view
  if (target.nodeName === "#text") {
	  target.parentElement?.prepend(dummy);
  }
  else {
	  target.prepend(dummy);
  }
  dummy.focus();
  this._updateState({ ariaNodeTarget: null });
}

So the element is removed only when the focus moves away from it, otherwise virtual cursor gets confused as well. That would not be good, right? tabindex is a bit trickier because, for example, making a section selected during outline navigation focusable will mean that next arrowDown on it will move to the next section, and such. But I suppose I'll try to set tabIndex on the lowest-level node possible (so, in that example, the header and not the section itself) and that should do it as well.

I'll open up a draft PR with all of my changes so far to make it easier to refer to specific parts.

@AbeJellinek
Copy link
Member

That code would definitely break EPUB CFIs, so if the tabIndex approach works, we should do it. The first visible element is accessible in EPUBs via closestElement(this.flow.startRange.startContainer) as in my example above.

If we need different things in PDF and EPUB/snapshot, the delegation approach I mentioned here would be preferable. reader.js shouldn't need to know anything about the internal structure of the views' iframes.

@abaevbog
Copy link
Contributor Author

abaevbog commented Sep 3, 2024

Gotcha, I'll drop all DOM manipulations for EPUB. It seems to be possible with a few tweaks, at least for the outline, which is a decent starting point. I'll do that and open a PR to make sure I'm on the right track.

@abaevbog abaevbog linked a pull request Sep 4, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging a pull request may close this issue.

3 participants