-
Notifications
You must be signed in to change notification settings - Fork 32
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
miscellaneous a11y improvements #137
base: master
Are you sure you want to change the base?
Changes from 2 commits
6d411f1
8ae1105
e8f1a15
577a916
a4af150
9ad399f
cadc0ee
f28c24c
2bd6260
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -186,6 +186,10 @@ class Reader { | |
entireWord: false, | ||
result: null | ||
}, | ||
a11yVirtualCursorTarget: { | ||
node: null, | ||
ts: null | ||
} | ||
}; | ||
|
||
if (options.secondaryViewState) { | ||
|
@@ -655,33 +659,18 @@ class Reader { | |
this._onTextSelectionAnnotationModeChange(mode); | ||
} | ||
|
||
// Announce the index of current search result to screen readers | ||
setA11ySearchResultMessage(primaryView) { | ||
let result = (primaryView ? this._state.primaryViewFindState : this._state.secondaryViewFindState).result; | ||
if (!result) return; | ||
let searchIndex = `${this._getString("pdfReader.searchResultIndex")}: ${result.index + 1}`; | ||
let totalResults = `${this._getString("pdfReader.searchResultTotal")}: ${result.total}`; | ||
this.setA11yMessage(`${searchIndex}. ${totalResults}`); | ||
} | ||
|
||
findNext(primary) { | ||
if (primary === undefined) { | ||
primary = this._lastViewPrimary; | ||
} | ||
(primary ? this._primaryView : this._secondaryView).findNext(); | ||
setTimeout(() => { | ||
this.setA11ySearchResultMessage(primary); | ||
}); | ||
} | ||
|
||
findPrevious(primary) { | ||
if (primary === undefined) { | ||
primary = this._lastViewPrimary; | ||
} | ||
(primary ? this._primaryView : this._secondaryView).findPrevious(); | ||
setTimeout(() => { | ||
this.setA11ySearchResultMessage(primary); | ||
}); | ||
} | ||
|
||
toggleEPUBAppearancePopup({ open }) { | ||
|
@@ -797,6 +786,7 @@ class Reader { | |
this.focusView(primary); | ||
// A workaround for Firefox/Zotero because iframe focusing doesn't trigger 'focusin' event | ||
this._focusManager._closeFindPopupIfEmpty(); | ||
this.placeA11yVirtualCursor(); | ||
}; | ||
|
||
let onRequestPassword = () => { | ||
|
@@ -862,6 +852,35 @@ class Reader { | |
this.setA11yMessage(annotationContent); | ||
} | ||
|
||
// Add page number as aria-label to provided node to improve screen reader navigation | ||
let setA11yNavContent = (node, pageIndex) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this function inside reader.js and not in the views, because views don't have localized strings? If so, we could introduce a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, exactly.
That would be quite useful! All a11y-related tweaks require some kind of additional strings for context, so not having to drag it all from the reader would be nice There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I removed the I then did try to just expose the |
||
node.setAttribute('aria-label', `${this._getString("pdfReader.page")}: ${pageIndex}`); | ||
}; | ||
|
||
// Set which node should receive focus when the focus enters the reader to | ||
// help screen readers place virtual cursor at the right location | ||
let setA11yVirtualCursorTarget = (node) => { | ||
if (node && node !== this._state.a11yVirtualCursorTarget.node) { | ||
this._updateState({ a11yVirtualCursorTarget: { node, ts: Date.now() } }); | ||
} | ||
// Clear the cursor only half a second after it was set. It ensures the | ||
// target is not cleared by scrolling of the document during outline navigation. | ||
// Particularly important for snapshots where a random scroll event would fire after | ||
// debounceUntilScrollFinishes is done. In all other instances of scrolling, | ||
// the virtual cursor target is cleared | ||
if (node === null && Date.now() - this._state.a11yVirtualCursorTarget.ts > 500) { | ||
this._updateState({ a11yVirtualCursorTarget: { node: null, ts: null } }); | ||
} | ||
}; | ||
|
||
// Announce the search index, page and snippet of the search result | ||
let a11yAnnounceSearchMessage = (index, total, pageLabel, snippet) => { | ||
let searchIndex = `${this._getString("pdfReader.searchResultIndex")}: ${index + 1}.`; | ||
let totalResults = `${this._getString("pdfReader.searchResultTotal")}: ${total}.`; | ||
let page = pageLabel !== null ? `${this._getString("pdfReader.page")}: ${pageLabel}.` : ""; | ||
this.setA11yMessage(`${searchIndex} ${totalResults} ${snippet || ""} ${page}`); | ||
}; | ||
|
||
let data; | ||
if (this._type === 'pdf') { | ||
data = this._data; | ||
|
@@ -908,7 +927,9 @@ class Reader { | |
onTabOut, | ||
onKeyDown, | ||
onKeyUp, | ||
onFocusAnnotation | ||
onFocusAnnotation, | ||
setA11yVirtualCursorTarget, | ||
a11yAnnounceSearchMessage | ||
}; | ||
|
||
if (this._type === 'pdf') { | ||
|
@@ -939,6 +960,7 @@ class Reader { | |
fontFamily: this._state.fontFamily, | ||
hyphenate: this._state.hyphenate, | ||
onEPUBEncrypted, | ||
setA11yNavContent, | ||
}); | ||
} else if (this._type === 'snapshot') { | ||
view = new SnapshotView({ | ||
|
@@ -965,6 +987,46 @@ class Reader { | |
document.getElementById("a11yAnnouncement").innerText = a11yMessage; | ||
} | ||
|
||
// Make a11yVirtualCursorTarget node set previously focusable and | ||
// focus it to help screen readers understand where the virtual cursor needs to | ||
// be positioned. This is required because screen readers are not aware of | ||
// scroll positioning, so without this, the virtual cursor will always land | ||
// at the start of the document. | ||
placeA11yVirtualCursor() { | ||
let target = this._state.a11yVirtualCursorTarget.node; | ||
let doc = this._lastView._iframe.contentDocument; | ||
// If the target is a text node, use its parent (e.g. <p> or <h>) | ||
if (target?.nodeType === Node.TEXT_NODE) { | ||
target = target.parentNode; | ||
} | ||
if (!target || !doc.contains(target)) return; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the iframe doesn't contain the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's certainly not intended but I think there can be some odd instances of when it would happen without something completely breaking... For example, if one uses the split horizontal or vertical views. One can navigate the outline, which will remember |
||
// Make it temporarily focusable | ||
target.setAttribute("tabindex", "-1"); | ||
target.focus(); | ||
|
||
// On blur or keypress, blur it | ||
if (doc.activeElement == target) { | ||
target.addEventListener("blur", (_) => { | ||
target.removeAttribute("tabindex"); | ||
}); | ||
target.addEventListener("keydown", (_) => { | ||
target.blur(); | ||
}); | ||
// Keypress may not fire if screen reader is being used, in which | ||
// case remove tabindex next time the page content is scrolled | ||
let cleanUpOnScroll = (_) => { | ||
target.blur(); | ||
doc.removeEventListener("scroll", cleanUpOnScroll); | ||
}; | ||
doc.addEventListener("scroll", cleanUpOnScroll); | ||
} | ||
// If the focus didn't take, make sure temp tabindex is removed | ||
else { | ||
target.removeAttribute("tabindex"); | ||
} | ||
this._updateState({ a11yVirtualCursorTarget: { node: null, ts: null } }); | ||
} | ||
|
||
getUnsavedAnnotations() { | ||
|
||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to expose nodes from within the view’s iframe? Shouldn’t the view itself track and manage its virtual cursor independently from the reader UI? It seems like all the
a11yVirtualCursorTarget
related code could be moved into the views.What happens if the view is split and both views modify the
a11yVirtualCursorTarget
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That actually is a fair point... I'll offload the rest of virtual cursor logic into the views.
I thought of the split views as a very unlikely edge case because this is done just to help screen readers figure out where to look, so I think a split view is somewhat unlikely in this context. In a scenario that it does happen, I thought the later update would get the priority (as it's the view currently worked on), and if through some obscure focus movements we enter a view that the cursor target does not belongs to, we do nothing. But, yeah, if each frame keeps track of the node itself, this is no longer relevant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But after removing
a11yVirtualCursorTarget
it now should be possible to moveplaceA11yVirtualCursor
into views as well? Because it simply retrieves a target element from the view and doesn’t appear to interact with anything outside the iframe. If it needs to be triggered on focus, note that the views have afocus()
method.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It totally is.
placeA11yVirtualCursor
is the same for all views though (at least as of now) and has some amount of logic. So I thought it would be better to avoid duplicating code in case we want to change something later to avoid having to edit it in multiple places. There is some duplication now already for the getter and setter of thea11yVirtualCursorTarget
but those don't have as much logic so I figured those are not a big issue.Is duplication OK in this instance? Or is there a better place for views to share this function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you just want to reuse the same function across views, you can add it to
src/common/lib/utilities.js
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, gotcha. I moved
placeA11yVirtualCursor
to utils, so now we don't have anythingvirtualCursorTarget
-related in the Reader. With some additional refactoring to simplify things, all the logic is pushed into the views, and there is actually pretty much no duplication left. So that's good!