Skip to content

Commit

Permalink
aria and focus edits for vpat
Browse files Browse the repository at this point in the history
- vpat 60: added role=button and aria-label to X clear
icon of search annotations
- vpat 61: hide magnifying glass of search annotations field
from screen readers
- vpat 62: explicitly refocus search annotations input field
on X click to not loose focus (possible with voiceover)
- vpat 64: added aria labels to annotation preview inputs in sidebar
- vpat 65: added title for context menu button
- vpat 69: aria-description for "tags" button.
It will always announce that this button can be clicked
to manage tags regardless what the current label is.
Note, there is a warning logged that aria-description
in not a valid aria property - it is a bug in react 17.
- vpat 71: properly announce annotations. Added listbox and option roles
to help screen readers handle cursor movements. aria-labelledby of annotation
points at header and aria-describedby points at the editable aria of
the annotation so that the announcement is "Page ..." followed by the content of
the annotation or the comment.
- vpat:72 add tab semantics to the sidebar component
vpat 72 states that we should have an audible announcement
after a new tab was selected. This does not seem exactly correct
per https://www.w3.org/WAI/WCAG21/Understanding/status-messages.html#excepted-examples,
however the sidebar should still have proper tabbar semantics
to help screen readers handle focus. That way, when a different
tab is selected, the screen readers tend to repeat the title
of the tab with a note that it is selected, so this should
address that concern.
- vpat 73: aria properties to announce thumbnails in the side bar
- vpat 74: added description and state to tags. Added aria-description
to tags to announce that this will filter annotations by
this tag. Note that there is a warning logged that aria-description
is not a valid aria property - it is a bug in react 17.
Added role=checkbox and aria-checked state to tags to indicate which ones are toggled on
- vpat 79: aria label for "Edit page number" input

- make annotation text focusable without editing
added tabstop parameter to ExpandableEditor that
will add data-tabstop=1 and tabindex=-1 if annotation is focused
regardless of readonly state. It allows us to focus the annotation
text via tabbing without breaking existing behavior of making
the annotation text focusable and editable only on double-click.
  • Loading branch information
abaevbog committed Jun 14, 2024
1 parent f0edb3f commit e2494a5
Show file tree
Hide file tree
Showing 9 changed files with 68 additions and 10 deletions.
10 changes: 8 additions & 2 deletions src/common/components/common/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,15 +300,19 @@ let Content = React.forwardRef((props, ref) => {
return (
<Fragment>
<div
id={props.id}
ref={innerRef}
suppressContentEditableWarning={true}
className="content"
contentEditable={!props.readOnly}
dir="auto"
placeholder={props.placeholder}
data-tabstop={!props.readOnly ? 1 : undefined}
tabIndex={!props.readOnly ? -1 : undefined}
data-tabstop={props.tabstop || (!props.readOnly ? 1 : undefined)}
tabIndex={(!props.readOnly || props.tabstop) ? -1 : undefined}
onInput={handleInput}
role="textbox"
aria-label={props.ariaLabel}
aria-readonly={props.readOnly}
/>
<div className="renderer" ref={rendererRef}></div>
</Fragment>
Expand All @@ -334,6 +338,8 @@ function Editor(props) {
enableRichText={props.enableRichText}
placeholder={props.placeholder}
onChange={props.onChange}
ariaLabel={props.ariaLabel}
tabstop={props.tabstop}
/>
</div>
);
Expand Down
11 changes: 11 additions & 0 deletions src/common/components/common/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export function PopupPreview(props) {
data-tabstop={!props.readOnly ? true : undefined}
tabIndex={-1}
className="more"
title={intl.formatMessage({ id: 'pdfReader.openMenu' })}
disabled={props.readOnly}
onClick={handleClickMore}
>{props.readOnly ? <IconLock/> : <IconOptions/>}</button>
Expand All @@ -102,6 +103,7 @@ export function PopupPreview(props) {
<Editor
id={annotation.id}
text={annotation.comment}
ariaLabel={intl.formatMessage({ id: 'pdfReader.annotationComment' })}
placeholder={props.readOnly ? intl.formatMessage({ id: 'pdfReader.readOnly' })
: intl.formatMessage({ id: 'pdfReader.addComment' })}
readOnly={props.readOnly}
Expand All @@ -116,6 +118,8 @@ export function PopupPreview(props) {
className="tags"
data-tabstop={1}
onClick={handleTagsClick}
aria-description={intl.formatMessage({ id: 'pdfReader.manageTags' })}
aria-haspopup={true}
>{annotation.tags.length ? annotation.tags.map((tag, index) => (
<span
className="tag" key={index}
Expand Down Expand Up @@ -215,7 +219,9 @@ export function SidebarPreview(props) {
id={annotation.id}
text={annotation.text}
placeholder={intl.formatMessage({ id: 'pdfReader.noExtractedText' })}
ariaLabel={intl.formatMessage({ id: 'pdfReader.annotationText' })}
readOnly={props.readOnly || state !== 3}
tabstop={state >= 1}
expanded={props.state >= 2}
enableRichText={annotation.type !== 'text'}
onChange={handleTextChange}
Expand All @@ -238,6 +244,7 @@ export function SidebarPreview(props) {
readOnly={props.readOnly || !(state === 1 || state === 2 || state === 3)}
expanded={state >= 1}
placeholder={intl.formatMessage({ id: 'pdfReader.addComment' })}
ariaLabel={intl.formatMessage({ id: 'pdfReader.annotationComment' })}
enableRichText={annotation.type !== 'text'}
onChange={handleCommentChange}
/>
Expand Down Expand Up @@ -287,6 +294,7 @@ export function SidebarPreview(props) {
className="page"
onClick={handlePageLabelClick}
onDoubleClick={handlePageLabelDoubleClick}
id={`page_${annotation.id}`}
>
<div><FormattedMessage id="pdfReader.page"/></div>
<div className="label">{annotation.pageLabel || '-'}</div>
Expand All @@ -305,6 +313,7 @@ export function SidebarPreview(props) {
tabIndex={props.selected && !props.readOnly ? -1 : undefined}
className="more"
disabled={props.readOnly}
title={intl.formatMessage({ id: 'pdfReader.openMenu' })}
onClick={handleClickMore}
// Make sure 'more' button focusing never triggers annotation element focusing,
// which triggers annotation selection
Expand All @@ -331,6 +340,8 @@ export function SidebarPreview(props) {
onClick={e => handleSectionClick(e, 'tags')}
draggable={true}
onDragStart={handleDragStart}
aria-haspopup={true}
aria-description={intl.formatMessage({ id: 'pdfReader.manageTags' })}
>{tags}</button>
)}
</div>
Expand Down
4 changes: 3 additions & 1 deletion src/common/components/modal-popup/label-popup.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import React, { useState, useEffect, useRef } from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';
import TooltipPopup from './common/tooltip-popup';


Expand Down Expand Up @@ -73,6 +73,7 @@ function LabelPopup({ params, onUpdateAnnotations, onClose }) {
let [checked, setChecked] = useState(data.checked);
let [auto, setAuto] = useState(false);
let inputRef = useRef();
const intl = useIntl();

useEffect(() => {
inputRef.current.focus();
Expand Down Expand Up @@ -232,6 +233,7 @@ function LabelPopup({ params, onUpdateAnnotations, onClose }) {
maxLength={32}
onChange={handleChange}
onKeyDown={handleInputKeydown}
aria-label={intl.formatMessage({ id: "pdfReader.editPageNumber"})}
/>
</div>
<div className="column second">
Expand Down
11 changes: 10 additions & 1 deletion src/common/components/sidebar/annotations-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ function Selector({ tags, colors, authors, onContextMenu, onClickTag, onClickCol
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={(event) => handleDrop(event, null, color.color)}
role="checkbox"
aria-checked={color.selected}
aria-description={intl.formatMessage({ id: "pdfReader.tagSelectorMessage" })}
><IconColor16 color={color.color}/></button>
))}
</div>}
Expand All @@ -55,6 +58,9 @@ function Selector({ tags, colors, authors, onContextMenu, onClickTag, onClickCol
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={(event) => handleDrop(event, { name: tag.name, color: tag.color })}
role="checkbox"
aria-checked={tag.selected}
aria-description={intl.formatMessage({ id: "pdfReader.tagSelectorMessage" })}
>{!!tag.color && <span className="icon"><IconTagCircle color={tag.color}/></span>}{tag.name}</button>
))}
</div>}
Expand Down Expand Up @@ -82,6 +88,9 @@ const Annotation = React.memo((props) => {
data-sidebar-annotation-id={props.annotation.id}
onMouseDown={(event) => event.stopPropagation()}
onFocus={() => props.onFocus(props.annotation.id)}
role="option"
aria-labelledby={`page_${props.annotation.id}`}
aria-describedby={props.annotation.id}
>
<SidebarPreview
type={props.type}
Expand Down Expand Up @@ -447,7 +456,7 @@ const AnnotationsView = memo(React.forwardRef((props, ref) => {

return (
<React.Fragment>
<div id="annotations" className="annotations" data-tabstop={props.annotations.length ? 1 : undefined} onKeyDownCapture={handleKeyDown}>
<div id="annotations" role='listbox' className="annotations" data-tabstop={props.annotations.length ? 1 : undefined} onKeyDownCapture={handleKeyDown}>
{props.annotations.length
? filteredAnnotations.map(annotation => (
<Annotation
Expand Down
2 changes: 1 addition & 1 deletion src/common/components/sidebar/outline-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ function OutlineView({ outline, onNavigate, onOpenLink, onUpdate}) {
}

return (
<div className={cx('outline-view', { loading: outline === null })} data-tabstop="1">
<div className={cx('outline-view', { loading: outline === null })} data-tabstop="1" id="outlineView" role="tabpanel" aria-labelledby="viewOutline">
{outline === null ? <div className="spinner"/> : renderItems(outline)}
</div>
);
Expand Down
11 changes: 9 additions & 2 deletions src/common/components/sidebar/search-box.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,16 @@ function SearchBox({ query, placeholder, onInput }) {
inputRef.current.focus();
}

function handleClearButton(event) {
handleClear(event);
// Make sure focus remain on the input - it can get lost if the button is clicked
// via screen readers' cursor
inputRef.current.focus();
}

return (
<div ref={inputRef2} className={`search-box ${expanded ? 'expanded' : ''}`}>
<div className="btn magnifier" onClick={handleMagnifierClick}>{expanded ? <IconMagnifier16/> : <IconMagnifier20/>}</div>
<div className="btn magnifier" onClick={handleMagnifierClick} aria-hidden="true">{expanded ? <IconMagnifier16/> : <IconMagnifier20/>}</div>
<input
ref={inputRef}
id="searchInput"
Expand All @@ -68,7 +75,7 @@ function SearchBox({ query, placeholder, onInput }) {
onFocus={handleFocus}
onBlur={handleBlur}
/>
{query.length !== 0 && <div className="btn clear" onClick={handleClear}><IconSearchCancel/></div>}
{query.length !== 0 && <div className="btn clear" role="button" aria-label={intl.formatMessage({ id: 'general.clear' })} onClick={handleClearButton}><IconSearchCancel/></div>}
</div>
);
}
Expand Down
13 changes: 11 additions & 2 deletions src/common/components/sidebar/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ function Sidebar(props) {
return (
<div id="sidebarContainer" className="sidebarOpen">
<div className="sidebar-toolbar">
<div className="start" data-tabstop={1}>
<div className="start" data-tabstop={1} role="tablist">
{props.type === 'pdf' &&
<button
id="viewThumbnail"
className={cx('toolbar-button', { active: props.view === 'thumbnails' })}
title="Show Thumbnails" tabIndex={-1}
onClick={() => props.onChangeView('thumbnails')}
role="tab"
aria-selected={props.view === 'thumbnails' }
aria-controls='thumbnailsView'
><IconThumbnails/></button>
}
<button
Expand All @@ -36,13 +39,19 @@ function Sidebar(props) {
title="Show Annotations"
tabIndex={-1}
onClick={() => props.onChangeView('annotations')}
role="tab"
aria-selected={props.view === 'annotations' }
aria-controls='annotationsView'
><IconAnnotations/></button>
<button
id="viewOutline"
className={cx('toolbar-button', { active: props.view === 'outline' })}
title="Show Document Outline (double-click to expand/collapse all items)"
tabIndex={-1}
onClick={() => props.onChangeView('outline')}
role="tab"
aria-selected={props.view === 'outline' }
aria-controls='outlineView'
><IconOutline/></button>
</div>
<div className="end">
Expand All @@ -57,7 +66,7 @@ function Sidebar(props) {
</div>
<div id="sidebarContent" className="sidebar-content">
{props.view === 'thumbnails' && props.thumbnailsView}
{props.view === 'annotations' && <div id="annotationsView">{props.annotationsView}</div>}
{props.view === 'annotations' && <div id="annotationsView" role="tabpanel" aria-labelledby='viewAnnotations'>{props.annotationsView}</div>}
{props.view === 'outline' && props.outlineView}
</div>
</div>
Expand Down
10 changes: 9 additions & 1 deletion src/common/components/sidebar/thumbnails-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const Thumbnail = memo(({ thumbnail, selected, pageLabel, onContextMenu }) => {
className={cx('thumbnail', { selected })}
data-page-index={thumbnail.pageIndex}
onContextMenu={onContextMenu}
role="option"
aria-label={pageLabel}
aria-selected={selected}
id={`thumbnail_${thumbnail.pageIndex}`}
>
<div className="image">
{thumbnail.image
Expand Down Expand Up @@ -199,7 +203,7 @@ function ThumbnailsView(props) {
}, [onOpenThumbnailContextMenu, selected]);

return (
<div className="thumbnails-view">
<div id="thumbnailsView" className="thumbnails-view" role="tabpanel" aria-labelledby="viewThumbnail">
{platform === 'web' && (
<div className="thumbnails-header">
<FormattedMessage id="pdfReader.selectedPages" values={ { count: selected.length }} />
Expand All @@ -219,6 +223,10 @@ function ThumbnailsView(props) {
onMouseDown={handleMouseDown}
ref={containerRef}
tabIndex={-1}
role='listbox'
aria-label={intl.formatMessage({ id: "pdfReader.thumbnails" })}
aria-activedescendant={`thumbnail_${selected[selected.length-1]}`}
aria-multiselectable="true"
>
{props.thumbnails.map((thumbnail, index) => {
let pageLabel = props.pageLabels[index] || (index + 1).toString();
Expand Down
6 changes: 6 additions & 0 deletions src/en-us.strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,13 @@ export default {
'pdfReader.noAnnotations': 'Create an annotation to see it in the sidebar',
'pdfReader.noExtractedText': 'No extracted text',
'pdfReader.addComment': 'Add comment',
'pdfReader.annotationComment': 'Annotation comment',
'pdfReader.annotationText': 'Annotation text',
'pdfReader.manageTags' : 'Click to manage tags.',
'pdfReader.openMenu' : 'Open menu',
'pdfReader.thumbnails' : 'Thumbnails',
'pdfReader.addTags': 'Add tags…',
'pdfReader.tagSelectorMessage' : 'Filter annotations by this tag',
'pdfReader.highlightText': 'Highlight Text',
'pdfReader.underlineText': 'Underline Text',
'pdfReader.addNote': 'Add Note',
Expand Down

0 comments on commit e2494a5

Please sign in to comment.