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

miscellaneous a11y improvements #137

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions src/common/components/sidebar/outline-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function Item({ item, id, children, onOpenLink, onUpdate, onSelect }) {
let { expanded, active } = item;

return (
<li>
<li id={`outline-${id}`} aria-label={item.title}>
<div
className={cx('item', { expandable: !!item.items?.length, expanded, active })}
data-id={id}
Expand Down Expand Up @@ -76,20 +76,20 @@ function OutlineView({ outline, onNavigate, onOpenLink, onUpdate}) {
}
}

function handleKeyDown(event) {
let { key } = event;

let list = [];
function flatten(items) {
for (let item of items) {
list.push(item);
if (item.items && item.expanded) {
flatten(item.items);
}
function flatten(items, list = []) {
for (let item of items) {
list.push(item);
if (item.items && item.expanded) {
flatten(item.items, list);
}
}
return list;
}

function handleKeyDown(event) {
let { key } = event;

flatten(outline);
let list = flatten(outline);

let currentIndex = list.findIndex(x => x.active);
let currentItem = list[currentIndex];
Expand Down Expand Up @@ -166,16 +166,18 @@ function OutlineView({ outline, onNavigate, onOpenLink, onUpdate}) {
);
}

let active = flatten(outline || []).findIndex(item => item.active);
return (
<div
ref={containerRef}
className={cx('outline-view', { loading: outline === null })}
data-tabstop="1"
tabIndex={-1}
id="outlineView"
role="tabpanel"
role="listbox"
aria-labelledby="viewOutline"
onKeyDown={handleKeyDown}
aria-activedescendant={active !== -1 ? `outline-${active}` : null}
>
{outline === null ? <div className="spinner"/> : renderItems(outline)}
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/common/components/sidebar/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function Sidebar(props) {
}

return (
<div id="sidebarContainer" className="sidebarOpen">
<div id="sidebarContainer" className="sidebarOpen" role="application">
<div className="sidebar-toolbar">
<div className="start" data-tabstop={1} role="tablist">
{props.type === 'pdf' &&
Expand Down Expand Up @@ -72,7 +72,7 @@ function Sidebar(props) {
<div id="annotationsView" role="tabpanel" aria-labelledby="viewAnnotations" className={cx("viewWrapper", { hidden: props.view !== 'annotations'})}>
{props.annotationsView}
</div>
<div className={cx("viewWrapper", { hidden: props.view !== 'outline'})}>
<div className={cx("viewWrapper", { hidden: props.view !== 'outline' })} role="tabpanel">
{props.outlineView}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/common/components/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function Toolbar(props) {
}

return (
<div className="toolbar" data-tabstop={1}>
<div className="toolbar" data-tabstop={1} role="application">
<div className="start">
<button
id="sidebarToggle"
Expand Down
2 changes: 2 additions & 0 deletions src/common/defines.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ export const MIN_IMAGE_ANNOTATION_SIZE = 10; // pt
export const DEBOUNCE_STATE_CHANGE = 300; // ms
export const DEBOUNCE_STATS_CHANGE = 100; // ms
export const DEBOUNCE_FIND_POPUP_INPUT = 500; // ms

export const A11Y_VIRT_CURSOR_DEBOUNCE_LENGTH = 100; // ms
36 changes: 36 additions & 0 deletions src/common/lib/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,3 +332,39 @@ if (!Array.prototype.findLastIndex) {
return -1;
};
}

/**
* Explicitly focus a given node within the view to force screen readers to move
* their virtual cursors to that element. Screen readers just look at rendered content
* so without this any navigation done via outline/Find in/page input in toolbar gets
* undone by virtual cursor either remaining where it was or even jumping to the beginning of content.
* @param target - node to focus from the view. Views keep track of it in _a11yVirtualCursorTarget obj.
*/
export async function placeA11yVirtualCursor(target) {
// Can't focus a textnode, so grab its parent (e.g. <p>)
if (target?.nodeType === Node.TEXT_NODE) {
target = target.parentNode;
}
if (!target) return;
let doc = target.ownerDocument;
// Make it temporarily focusable
target.setAttribute("tabindex", "-1");
target.setAttribute("a11y-cursor-target", true);
target.focus();
// If focus didn't take, remove tabindex and stop
if (doc.activeElement != target) {
target.removeAttribute("tabindex");
return;
}
// Remove focus when element looses focus
target.addEventListener("blur", (_) => {
target.removeAttribute("tabindex");
target.removeAttribute("a11y-cursor-target");
});
// Blur the target on any keypress so that one can still scroll content with
// arrowUp/Down. Otherwise, all keydown events land on the target and
// nothing happens
target.addEventListener("keydown", (_) => {
target.blur();
});
}
33 changes: 17 additions & 16 deletions src/common/reader.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ class Reader {
caseSensitive: false,
entireWord: false,
result: null
},
}
};

if (options.secondaryViewState) {
Expand Down Expand Up @@ -655,33 +655,30 @@ 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}`);
}
// Announce info about current search result to screen readers.
// FindState is updated multiple times while navigating between results
// so debounce is used to fire only after the last update.
a11yAnnounceSearchMessage = debounce((findStateResult) => {
if (!findStateResult) return;
let { index, total, currentPageLabel, currentSnippet } = findStateResult;
let searchIndex = `${this._getString("pdfReader.searchResultIndex")}: ${index + 1}.`;
let totalResults = `${this._getString("pdfReader.searchResultTotal")}: ${total}.`;
let page = currentPageLabel ? `${this._getString("pdfReader.page")}: ${currentPageLabel}.` : "";
this.setA11yMessage(`${searchIndex} ${totalResults} ${currentSnippet || ""} ${page}`);
}, 100);

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 }) {
Expand Down Expand Up @@ -828,6 +825,7 @@ class Reader {

let onSetFindState = (params) => {
this._updateState({ [primary ? 'primaryViewFindState' : 'secondaryViewFindState']: params });
this.a11yAnnounceSearchMessage(params.result);
};

let onSelectAnnotations = (ids, triggeringEvent) => {
Expand Down Expand Up @@ -862,6 +860,8 @@ class Reader {
this.setA11yMessage(annotationContent);
}

let getLocalizedString = (name) => this._getString(name);

let data;
if (this._type === 'pdf') {
data = this._data;
Expand Down Expand Up @@ -908,7 +908,8 @@ class Reader {
onTabOut,
onKeyDown,
onKeyUp,
onFocusAnnotation
onFocusAnnotation,
getLocalizedString
};

if (this._type === 'pdf') {
Expand Down
5 changes: 4 additions & 1 deletion src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,10 @@ export type FindState = {
total: number,
index: number,
// Mobile app lists all results in a popup
snippets: string[]
snippets: string[],
// Used for a11y notifications
currentSnippet: string,
currentPageLabel: string
} | null;
};

Expand Down
18 changes: 17 additions & 1 deletion src/dom/common/dom-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
import { getSelectionRanges } from "./lib/selection";
import { FindProcessor } from "./lib/find";
import { SELECTION_COLOR } from "../../common/defines";
import { debounceUntilScrollFinishes, isMac, isSafari } from "../../common/lib/utilities";
import { debounceUntilScrollFinishes, isMac, isSafari, placeA11yVirtualCursor } from "../../common/lib/utilities";
import {
closestElement,
isElement
Expand Down Expand Up @@ -132,6 +132,8 @@ abstract class DOMView<State extends DOMViewState, Data> {

protected _resizing = false;

protected _a11yVirtualCursorTarget: Node | null;

scale = 1;

protected constructor(options: DOMViewOptions<State, Data>) {
Expand All @@ -152,6 +154,7 @@ abstract class DOMView<State extends DOMViewState, Data> {
onUpdate: () => this._updateViewStats(),
onNavigate: location => this.navigate(location, { skipHistory: true, behavior: 'auto' }),
});
this._a11yVirtualCursorTarget = null;

this._iframe = document.createElement('iframe');
this._iframe.sandbox.add('allow-same-origin', 'allow-modals');
Expand Down Expand Up @@ -1058,6 +1061,9 @@ abstract class DOMView<State extends DOMViewState, Data> {

this._options.onSetOverlayPopup();

// If we marked a node as future focus target for screen readers, clear it to avoid scrolling to it
this._a11yVirtualCursorTarget = null;

// Create note annotation on pointer down event, if note tool is active.
// The note tool will be automatically deactivated in reader.js,
// because this is what we do in PDF reader
Expand Down Expand Up @@ -1138,6 +1144,11 @@ abstract class DOMView<State extends DOMViewState, Data> {
this._renderAnnotations();
this._repositionPopups();
});
// If there exists a focused node for screen readers, make sure it gets blurred
let cursorTarget = this._iframeDocument.querySelector("[a11y-cursor-target]");
if (cursorTarget) {
(cursorTarget as HTMLElement).blur();
}
}

protected _handleScrollCapture(event: Event) {
Expand Down Expand Up @@ -1194,6 +1205,10 @@ abstract class DOMView<State extends DOMViewState, Data> {

private _handleFocus() {
this._options.onFocus();

// Help screen readers understand where to place virtual cursor
placeA11yVirtualCursor(this._a11yVirtualCursorTarget);
this._a11yVirtualCursorTarget = null;
}

private _preventNextClickEvent() {
Expand Down Expand Up @@ -1412,6 +1427,7 @@ export type DOMViewOptions<State extends DOMViewState, Data> = {
onKeyUp: (event: KeyboardEvent) => void;
onKeyDown: (event: KeyboardEvent) => void;
onEPUBEncrypted: () => void;
getLocalizedString: (name: string) => string;
data: Data & {
buf?: Uint8Array,
url?: string
Expand Down
5 changes: 4 additions & 1 deletion src/dom/common/lib/find/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,15 @@ class DefaultFindProcessor implements FindProcessor {
private _setFindState() {
if (this._cancelled) return;
if (this._onSetFindState) {
let snippets = this.getSnippets();
this._onSetFindState({
...this.findState,
result: {
total: this._buf.length,
index: this._pos === null ? 0 : this._pos,
snippets: this.getSnippets(),
snippets: snippets,
currentPageLabel: "",
currentSnippet: this._pos === null ? "" : snippets[this._pos]
}
});
}
Expand Down
3 changes: 3 additions & 0 deletions src/dom/common/lib/range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ export class PersistentRange {

endOffset: number;

collapsed: boolean;

constructor(range: AbstractRange) {
this.startContainer = range.startContainer;
this.startOffset = range.startOffset;
this.endContainer = range.endContainer;
this.endOffset = range.endOffset;
this.collapsed = range.collapsed;
}

toRange(): Range {
Expand Down
2 changes: 2 additions & 0 deletions src/dom/epub/defines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ export const DEFAULT_EPUB_APPEARANCE: EPUBAppearance = Object.freeze({
pageWidth: PageWidth.Normal,
useOriginalFont: false,
});

export const A11Y_VIRT_CURSOR_DEBOUNCE_LENGTH = 100;
Loading
Loading