Skip to content

Commit

Permalink
EPUB/Snapshot: Keyboard accessibility improvements for annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
AbeJellinek committed Nov 12, 2024
1 parent 7e5e447 commit cb88128
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 36 deletions.
12 changes: 10 additions & 2 deletions src/common/lib/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ export function normalizeKey(key, code) {
return key;
}

// Key combination taking into account layout and modifier keys
/**
* Key combination taking into account layout and modifier keys
* @param {KeyboardEvent} event
* @returns {string}
*/
export function getKeyCombination(event) {
let modifiers = [];
if (event.metaKey && isMac()) {
Expand Down Expand Up @@ -86,7 +90,11 @@ export function getKeyCombination(event) {
return modifiers.join('-');
}

// Physical key combination
/**
* Physical key combination
* @param {KeyboardEvent} event
* @returns {string}
*/
export function getCodeCombination(event) {
let modifiers = [];
if (event.metaKey && isMac()) {
Expand Down
3 changes: 2 additions & 1 deletion src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ export type FindState = {
total: number,
index: number,
// Mobile app lists all results in a popup
snippets: string[]
snippets: string[],
annotation?: NewAnnotation
} | null;
};

Expand Down
121 changes: 107 additions & 14 deletions src/dom/common/dom-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
Platform,
SelectionPopupParams,
Tool,
ToolType,
ViewStats,
WADMAnnotation,
} from "../../common/types";
Expand All @@ -40,9 +41,15 @@ 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 {
closestElement,
debounceUntilScrollFinishes,
getCodeCombination,
getKeyCombination,
isMac,
isSafari
} from "../../common/lib/utilities";
import {
closestElement, getContainingBlock,
isElement
} from "./lib/nodes";
import { debounce } from "../../common/lib/debounce";
Expand Down Expand Up @@ -301,7 +308,25 @@ abstract class DOMView<State extends DOMViewState, Data> {
if (!selection || selection.isCollapsed) {
return null;
}
let range = makeRangeSpanning(...getSelectionRanges(selection));
let range: Range;
if (type === 'highlight' || type === 'underline') {
range = makeRangeSpanning(...getSelectionRanges(selection));
}
else if (type === 'note') {
let element = closestElement(selection.getRangeAt(0).commonAncestorContainer);
if (!element) {
return null;
}
let blockElement = getContainingBlock(element);
if (!blockElement) {
return null;
}
range = this._iframeDocument.createRange();
range.selectNode(blockElement);
}
else {
return null;
}
return this._getAnnotationFromRange(range, type, color);
}

Expand Down Expand Up @@ -671,13 +696,11 @@ abstract class DOMView<State extends DOMViewState, Data> {
else {
let pos = supportsCaretPositionFromPoint()
&& caretPositionFromPoint(this._iframeDocument, event.clientX, event.clientY);
let node = pos ? pos.offsetNode : target;
// Expand to the closest block element
while (node.parentNode
&& (!isElement(node) || this._iframeWindow.getComputedStyle(node).display.includes('inline'))) {
node = node.parentNode;
}
range.selectNode(node);
let element = closestElement(pos ? pos.offsetNode : target);
if (!element) return null;
let blockElement = getContainingBlock(element);
if (!blockElement) return null;
range.selectNode(blockElement);
}
let rect = range.getBoundingClientRect();
if (rect.right <= 0 || rect.left >= this._iframeWindow.innerWidth
Expand All @@ -704,12 +727,12 @@ abstract class DOMView<State extends DOMViewState, Data> {
protected abstract _handleInternalLinkClick(link: HTMLAnchorElement): void;

protected _handleKeyDown(event: KeyboardEvent) {
let { key } = event;
let shift = event.shiftKey;

// To figure out if wheel events are pinch-to-zoom
this._isCtrlKeyDown = event.key === 'Control';

let key = getKeyCombination(event);
let code = getCodeCombination(event);

// Focusable elements in PDF view are annotations and overlays (links, citations, figures).
// Once TAB is pressed, arrows can be used to navigate between them
let focusableElements: HTMLElement[] = [];
Expand Down Expand Up @@ -737,7 +760,7 @@ abstract class DOMView<State extends DOMViewState, Data> {
// pass it to this._onKeyDown(event) below
return;
}
else if (shift && key === 'Tab') {
else if (key === 'Shift-Tab') {
if (focusedElement) {
focusedElement.blur();
}
Expand Down Expand Up @@ -790,6 +813,75 @@ abstract class DOMView<State extends DOMViewState, Data> {
}
}

if (this._selectedAnnotationIDs.length === 1
&& (key.endsWith('Shift-ArrowLeft')
|| key.endsWith('Shift-ArrowRight'))) {
let resizeStart = key.startsWith('Cmd-') || key.startsWith('Ctrl-');
let granularity = event.altKey ? 'word' : 'character';

let annotation = this._annotationsByID.get(this._selectedAnnotationIDs[0])!;
let selection = this._iframeDocument.getSelection()!;

let oldRange = this.toDisplayedRange(annotation.position)!;
selection.removeAllRanges();
selection.addRange(oldRange);
if (resizeStart) {
selection.collapseToStart();
}
else {
selection.collapseToEnd();
}
selection.modify(
'move',
key.endsWith('ArrowRight') ? 'right' : 'left',
granularity
);
let newRange = selection.getRangeAt(0);
if (resizeStart) {
newRange.setEnd(oldRange.endContainer, oldRange.endOffset);
}
else {
newRange.setStart(oldRange.startContainer, oldRange.startOffset);
}

if (newRange.collapsed) {
return;
}

annotation.position = this.toSelector(newRange)!;
this._options.onUpdateAnnotations([annotation]);
selection.removeAllRanges();

event.preventDefault();
return;
}

if (code === 'Ctrl-Alt-Digit1' || code === 'Ctrl-Alt-Digit2' || code === 'Ctrl-Alt-Digit3') {
let type: AnnotationType;
switch (code) {
case 'Ctrl-Alt-Digit1':
type = 'highlight';
break;
case 'Ctrl-Alt-Digit2':
type = 'underline';
break;
case 'Ctrl-Alt-Digit3':
type = 'note';
break;
}
let annotation = this._getAnnotationFromTextSelection(type, this._options.tools[type].color);
if (annotation) {
this._options.onAddAnnotation(annotation, true);
this._navigateToSelector(annotation.position, {
block: 'center',
behavior: 'smooth'
});
this._iframeWindow.getSelection()?.removeAllRanges();
}
event.preventDefault();
return;
}

// Pass keydown even to the main window where common keyboard
// shortcuts are handled i.e. Delete, Cmd-Minus, Cmd-f, etc.
this._options.onKeyDown(event);
Expand Down Expand Up @@ -1393,6 +1485,7 @@ export type DOMViewOptions<State extends DOMViewState, Data> = {
primary?: boolean;
mobile?: boolean;
container: Element;
tools: Record<ToolType, Tool>;
tool: Tool;
platform: Platform;
selectedAnnotationIDs: string[];
Expand Down
21 changes: 13 additions & 8 deletions src/dom/common/lib/find/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class DefaultFindProcessor implements FindProcessor {

private _initialPos: number | null = null;

private readonly _onSetFindState?: (state?: FindState) => void;
private readonly _onSetFindState?: (result: ResultArg) => void;

private readonly _annotationKeyPrefix?: string;

Expand All @@ -27,7 +27,7 @@ class DefaultFindProcessor implements FindProcessor {

constructor(options: {
findState: FindState,
onSetFindState?: (state?: FindState) => void,
onSetFindState?: (result: ResultArg) => void,
annotationKeyPrefix?: string,
}) {
this.findState = options.findState;
Expand Down Expand Up @@ -247,12 +247,10 @@ class DefaultFindProcessor implements FindProcessor {
if (this._cancelled) return;
if (this._onSetFindState) {
this._onSetFindState({
...this.findState,
result: {
total: this._buf.length,
index: this._pos === null ? 0 : this._pos,
snippets: this.getSnippets(),
}
total: this._buf.length,
index: this._pos === null ? 0 : this._pos,
snippets: this.getSnippets(),
range: this.current?.range
});
}
}
Expand Down Expand Up @@ -282,6 +280,13 @@ function normalize(s: string) {

export type FindAnnotation = Omit<DisplayedAnnotation, 'range'> & { range: PersistentRange };

export type ResultArg = {
total: number;
index: number;
snippets: string[];
range?: PersistentRange;
};

export type SearchContext = {
text: string;
charDataRanges: CharDataRange[];
Expand Down
15 changes: 14 additions & 1 deletion src/dom/epub/epub-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,20 @@ class EPUBView extends DOMView<EPUBViewState, EPUBViewData> {
this._find = new EPUBFindProcessor({
view: this,
findState: { ...state },
onSetFindState: this._options.onSetFindState,
onSetFindState: (result) => {
this._options.onSetFindState({
...state,
result: {
total: result.total,
index: result.index,
snippets: result.snippets,
annotation: (
result.range
&& this._getAnnotationFromRange(result.range.toRange(), 'highlight')
) ?? undefined
}
});
},
});
let startRange = (this.flow.startRange && new PersistentRange(this.flow.startRange)) ?? undefined;
let onFirstResult = () => this.findNext();
Expand Down
15 changes: 6 additions & 9 deletions src/dom/epub/find.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import DefaultFindProcessor, {
FindAnnotation,
FindProcessor,
FindResult
FindResult, ResultArg
} from "../common/lib/find";
import EPUBView from "./epub-view";
import SectionRenderer from "./section-renderer";
Expand All @@ -23,12 +23,12 @@ export class EPUBFindProcessor implements FindProcessor {

private _cancelled = false;

private readonly _onSetFindState?: (state?: FindState) => void;
private readonly _onSetFindState?: (result: ResultArg) => void;

constructor(options: {
view: EPUBView,
findState: FindState,
onSetFindState?: (state?: FindState) => void,
onSetFindState?: (result: ResultArg) => void,
}) {
this.view = options.view;
this.findState = options.findState;
Expand Down Expand Up @@ -179,12 +179,9 @@ export class EPUBFindProcessor implements FindProcessor {
snippets.push(...processor.getSnippets());
}
this._onSetFindState({
...this.findState,
result: {
total: this._totalResults,
index,
snippets,
}
total: this._totalResults,
index,
snippets,
});
}
}
Expand Down
15 changes: 14 additions & 1 deletion src/dom/snapshot/snapshot-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,20 @@ class SnapshotView extends DOMView<SnapshotViewState, SnapshotViewData> {
console.log('Initiating new search', state);
this._find = new DefaultFindProcessor({
findState: { ...state },
onSetFindState: this._options.onSetFindState,
onSetFindState: (result) => {
this._options.onSetFindState({
...state,
result: {
total: result.total,
index: result.index,
snippets: result.snippets,
annotation: (
result.range
&& this._getAnnotationFromRange(result.range.toRange(), 'highlight')
) ?? undefined
}
});
},
});
await this._find.run(
this._searchContext,
Expand Down

0 comments on commit cb88128

Please sign in to comment.