Skip to content

Commit

Permalink
vpat 81: instructions for keyboard interface (#142)
Browse files Browse the repository at this point in the history
- added aria-descriptions for buttons in the toolbar that tell what
  shortcuts exist and how to use them for different annotation types
- added aria-description to the input in Find in Document popup to
  tell that one can create annotations from search results
- when annotation is selected, announce a message saying that the
  annotation is selected, indicating that a popup appeared (if no
  sidebar), and telling how one can manipulate the annotation
  (more/resize/etc.)
- when annotation is added, announce that a new annotation has been
  created. It helps in cases like when a text annotation is added and
  text is immediately focused, or when underline annotation is created
  from Find popup and focus does not come back to the view to at least
  indicate that it did work.

A few minor functional tweaks:

- when annotation is added from Find in Document, close the popup and
  focus the view. Otherwise, one is likely to close popup with Escape
  and then they would have to tab to the annotation again.
- when a note is added via keyboard shortcut, do focus the comment,
  since the instruction has to be "Use ctrl-option-3 to add a note
  annotation", and if comment is not focused, one would create an empty
  note and then have to tab all the way to the sidebar and find it
  before being able to edit it.
- fix regression where Control-Option/Alt-1/2 would not work from the
  Find in Document popup, since the color was not being set.
  • Loading branch information
abaevbog authored Oct 12, 2024
1 parent 97dd5ab commit ffc5593
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 7 deletions.
1 change: 1 addition & 0 deletions src/common/components/reader-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ function View(props) {
onFindNext={handleFindNext}
onFindPrevious={handleFindPrevious}
onAddAnnotation={props.onAddAnnotation}
tools={props.tools}
/>
}
</div>
Expand Down
17 changes: 16 additions & 1 deletion src/common/components/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useIntl } from 'react-intl';
import cx from 'classnames';
import CustomSections from './common/custom-sections';
import { ReaderContext } from '../reader';

import { isMac } from '../lib/utilities';
import { IconColor20 } from './common/icons';

import IconSidebar from '../../../res/icons/20/sidebar.svg';
Expand Down Expand Up @@ -72,6 +72,15 @@ function Toolbar(props) {
}
}

// Add aria instructions on how to add annotations with keyboard
function _constructAriaDecription(number) {
// Cmd/Alt+Option+1/2
let underlineOrHighlight = number <= 2;
let instruction = intl.formatMessage({ id: `pdfReader.a11y${underlineOrHighlight ? 'Textual' : ''}AnnotationInstruction` });
let modifier = intl.formatMessage({ id: `pdfReader.a11yAnnotationModifier${isMac() ? 'Mac' : ''}` });
return `${instruction} ${modifier} - ${number}`;
}

return (
<div className="toolbar" data-tabstop={1}>
<div className="start">
Expand Down Expand Up @@ -177,6 +186,7 @@ function Toolbar(props) {
title={intl.formatMessage({ id: 'pdfReader.highlightText' })}
disabled={props.readOnly}
onClick={() => handleToolClick('highlight')}
aria-description={_constructAriaDecription(1)}
><IconHighlight/></button>
{ (platform !== 'web' || ['epub', 'snapshot'].includes(props.type)) && (
<button
Expand All @@ -185,6 +195,7 @@ function Toolbar(props) {
title={intl.formatMessage({ id: 'pdfReader.underlineText' })}
disabled={props.readOnly}
onClick={() => handleToolClick('underline')}
aria-description={_constructAriaDecription(2)}
><IconUnderline/></button>
)}
<button
Expand All @@ -195,6 +206,7 @@ function Toolbar(props) {
title={intl.formatMessage({ id: 'pdfReader.addNote' })}
disabled={props.readOnly}
onClick={() => handleToolClick('note')}
aria-description={_constructAriaDecription(3)}
><IconNote/></button>
{props.type === 'pdf' && platform !== 'web' && (
<button
Expand All @@ -203,6 +215,7 @@ function Toolbar(props) {
title={intl.formatMessage({ id: 'pdfReader.addText' })}
disabled={props.readOnly}
onClick={() => handleToolClick('text')}
aria-description={_constructAriaDecription(4)}
><IconText/></button>
)}
{props.type === 'pdf' && (
Expand All @@ -212,6 +225,7 @@ function Toolbar(props) {
title={intl.formatMessage({ id: 'pdfReader.selectArea' })}
disabled={props.readOnly}
onClick={() => handleToolClick('image')}
aria-description={_constructAriaDecription(5)}
><IconImage/></button>
)}
{props.type === 'pdf' && (
Expand All @@ -221,6 +235,7 @@ function Toolbar(props) {
title={intl.formatMessage({ id: 'pdfReader.draw' })}
disabled={props.readOnly}
onClick={() => handleToolClick('ink')}
aria-description={intl.formatMessage({ id: 'pdfReader.a11yAnnotationNotSupported' })}
><IconInk/></button>
)}
<div className="divider"/>
Expand Down
17 changes: 13 additions & 4 deletions src/common/components/view-popup/find-popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { DEBOUNCE_FIND_POPUP_INPUT } from '../../defines';
import IconChevronUp from '../../../../res/icons/20/chevron-up.svg';
import IconChevronDown from '../../../../res/icons/20/chevron-down.svg';
import IconClose from '../../../../res/icons/20/x.svg';
import { getCodeCombination, getKeyCombination } from '../../lib/utilities';
import { getCodeCombination, getKeyCombination, isMac } from '../../lib/utilities';

function FindPopup({ params, onChange, onFindNext, onFindPrevious, onAddAnnotation }) {
function FindPopup({ params, onChange, onFindNext, onFindPrevious, onAddAnnotation, tools }) {
const intl = useIntl();
const inputRef = useRef();
const preventInputRef = useRef(false);
Expand Down Expand Up @@ -75,13 +75,17 @@ function FindPopup({ params, onChange, onFindNext, onFindPrevious, onAddAnnotati
else if (code === 'Ctrl-Alt-Digit1') {
preventInputRef.current = true;
if (params.result?.annotation) {
onAddAnnotation({ ...params.result.annotation, type: 'highlight' }, true);
onAddAnnotation({ ...params.result.annotation, type: 'highlight', color: tools['highlight'].color }, true);
// Close popup after adding annotation
onChange({ ...params, popupOpen: false, active: false, result: null });
}
}
else if (code === 'Ctrl-Alt-Digit2') {
preventInputRef.current = true;
if (params.result?.annotation) {
onAddAnnotation({ ...params.result.annotation, type: 'underline' }, true);
onAddAnnotation({ ...params.result.annotation, type: 'underline', color: tools['underline'].color }, true);
// Close popup after adding annotation
onChange({ ...params, popupOpen: false, active: false, result: null });
}
}
}
Expand Down Expand Up @@ -112,6 +116,11 @@ function FindPopup({ params, onChange, onFindNext, onFindPrevious, onAddAnnotati
title={intl.formatMessage({ id: 'pdfReader.find' })}
className="toolbar-text-input"
placeholder="Find in document…"
aria-description={
intl.formatMessage({ id: 'pdfReader.a11yTextualAnnotationFindInDocumentInstruction' })
+ ` ${intl.formatMessage({ id: 'pdfReader.a11yAnnotationModifierControl' })} - ${intl.formatMessage({ id: `pdfReader.a11yAnnotationModifier${isMac() ? 'Mac' : ''}` })} - ${1}`
+ `, ${intl.formatMessage({ id: 'pdfReader.a11yAnnotationModifierControl' })} - ${intl.formatMessage({ id: `pdfReader.a11yAnnotationModifier${isMac() ? 'Mac' : ''}` })} - ${2}`
}
value={query !== null ? query : params.query}
tabIndex="-1"
data-tabstop={1}
Expand Down
42 changes: 41 additions & 1 deletion src/common/reader.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ class Reader {
onResizeSplitView={this.setSplitViewSize.bind(this)}
onAddAnnotation={(annotation, select) => {
annotation = this._annotationManager.addAnnotation(annotation);
// Tell screen readers the annotation was added after focus is settled
setTimeout(() => {
this.setA11yMessage(this._getString(`pdfReader.a11yAnnotationCreated.${annotation.type}`));
}, 100);
if (select) {
this.setSelectedAnnotations([annotation.id]);
} else {
Expand Down Expand Up @@ -307,6 +311,7 @@ class Reader {
onToggleContextPane={this._onToggleContextPane}
onChangeTextSelectionAnnotationMode={this.setTextSelectionAnnotationMode.bind(this)}
ref={this._readerRef}
tools={this._tools}
/>
</ReaderContext.Provider>
</IntlProvider>
Expand Down Expand Up @@ -781,6 +786,10 @@ class Reader {

let onAddAnnotation = (annotation, select) => {
annotation = this._annotationManager.addAnnotation(annotation);
// Tell screen readers the annotation was added after focus is settled
setTimeout(() => {
this.setA11yMessage(this._getString(`pdfReader.a11yAnnotationCreated.${annotation.type}`));
}, 100);
if (select) {
this.setSelectedAnnotations([annotation.id], true);
}
Expand Down Expand Up @@ -866,6 +875,10 @@ class Reader {
let onFocusAnnotation = (annotation) => {
if (!annotation) return;
// Announce the current annotation to screen readers
if (annotation.type == 'external-link') {
this.setA11yMessage(annotation.url);
return;
}
let annotationType = this._getString(`pdfReader.${annotation.type}Annotation`);
let annotationContent = `${annotationType}. ${annotation.text || annotation.comment}`;
this.setA11yMessage(annotationContent);
Expand Down Expand Up @@ -1178,10 +1191,11 @@ class Reader {
this._updateState({ selectedAnnotationIDs: ids });

// Don't navigate to annotation or focus comment if opening a context menu
// unless it is a note (so that one can type after creating it via shortcut, same as with text annotation)
if (!triggeringEvent || triggeringEvent.button !== 2) {
if (triggeredFromView) {
if (['note', 'highlight', 'underline'].includes(annotation.type)
&& !annotation.comment && (!triggeringEvent || !('key' in triggeringEvent))) {
&& !annotation.comment && (!triggeringEvent || annotation.type === 'note')) {
this._enableAnnotationDeletionFromComment = true;
setTimeout(() => {
let content;
Expand All @@ -1199,6 +1213,32 @@ class Reader {
this._lastView.navigate({ annotationID: annotation.id });
}
}
// After a small delay for focus to settle, announce to screen readers that annotation
// is selected and how one can manipulate it
setTimeout(() => {
let a11yAnnouncement = this._getString(`pdfReader.a11yAnnotationSelected.${annotation.type}`);
if (document.querySelector('.annotation-popup')) {
// add note that popup is opened
a11yAnnouncement += ' ' + this._getString('pdfReader.a11yAnnotationPopupAppeared');
}
if (['highlight', 'underline'].includes(annotation.type)) {
// tell how to edit highlight/underline annotations
a11yAnnouncement += ' ' + this._getString('pdfReader.a11yEditTextAnnotation') + ' ' + this._getString(`pdfReader.a11yAnnotationModifier${isMac() ? 'Mac' : ''}`);
}
else if (['note', 'text', 'image'].includes(annotation.type)) {
// tell how to move and resize remaining types
a11yAnnouncement += ' ' + this._getString('pdfReader.a11yMoveAnnotation');
if (['text', 'image'].includes(annotation.type)) {
a11yAnnouncement += ' ' + this._getString('pdfReader.a11yResizeAnnotation');
}
}

// only announce if the content view is focused. E.g. if comment in
// sidebar has focus, say nothing as it will not be relevant
if (document.activeElement.nodeName === 'IFRAME') {
this.setA11yMessage(a11yAnnouncement);
}
}, 100);
}
}
// Smoothly scroll to the annotation, if only one was selected
Expand Down
25 changes: 24 additions & 1 deletion src/en-us.strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,5 +190,28 @@ export default {
'pdfReader.convertToHighlight': 'Convert to Highlight',
'pdfReader.convertToUnderline': 'Convert to Underline',
'pdfReader.size': 'Size',
'pdfReader.merge': 'Merge'
'pdfReader.merge': 'Merge',
'pdfReader.a11yAnnotationModifierMac': 'Option',
'pdfReader.a11yAnnotationModifier': 'Alt',
'pdfReader.a11yAnnotationModifierControl': 'Control',
'pdfReader.a11yTextualAnnotationFindInDocumentInstruction': 'To turn a search result into a highlight or underline annotation, press',
'pdfReader.a11yTextualAnnotationInstruction': 'To annotate text via the keyboard, first use “Find in Document” to locate the phrase. Then, to turn the search result into an annotation, press Control -',
'pdfReader.a11yAnnotationInstruction': 'To add this annotation to the document, focus the document and press Control -',
'pdfReader.a11yAnnotationNotSupported': 'This annotation type cannot be created via the keyboard.',
'pdfReader.a11yMoveAnnotation': 'Use the arrow keys to move the annotation.',
'pdfReader.a11yEditTextAnnotation': 'To move the end of the text annotation, use the left/right arrow keys while holding Shift. To move the start of the annotation, use the arrow keys while holding Shift -',
'pdfReader.a11yResizeAnnotation': 'To resize the annotation, use the arrow keys while holding Shift.',
'pdfReader.a11yAnnotationPopupAppeared': 'Use Tab to navigate the annotation popup.',

"pdfReader.a11yAnnotationCreated.highlight": "Highlight annotation created",
"pdfReader.a11yAnnotationCreated.underline": "Underline annotation created",
"pdfReader.a11yAnnotationCreated.note": "Note annotation created",
"pdfReader.a11yAnnotationCreated.text": "Text annotation created",
"pdfReader.a11yAnnotationCreated.image": "Image annotation created",

"pdfReader.a11yAnnotationSelected.highlight": "Highlight annotation selected",
"pdfReader.a11yAnnotationSelected.underline": "Underline annotation selected",
"pdfReader.a11yAnnotationSelected.note": "Note annotation selected",
"pdfReader.a11yAnnotationSelected.text": "Text annotation selected",
"pdfReader.a11yAnnotationSelected.image": "Image annotation selected"
};

0 comments on commit ffc5593

Please sign in to comment.