Desktop: Accessibility: Add missing labels and role information to several controls (#10788)

pull/10794/head
Henry Heino 2024-07-28 06:53:32 -07:00 committed by GitHub
parent 6d92e982dc
commit b108bf799d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 54 additions and 24 deletions

View File

@ -174,7 +174,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
textDecoration: 'none', textDecoration: 'none',
backgroundColor: theme.backgroundColor, backgroundColor: theme.backgroundColor,
padding: '.14em', padding: '.14em',
display: 'flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginLeft: '0.5em', marginLeft: '0.5em',
@ -281,11 +281,13 @@ class NotePropertiesDialog extends React.Component<Props, State> {
public createNoteField(key: keyof FormNote, value: any) { public createNoteField(key: keyof FormNote, value: any) {
const styles = this.styles(this.props.themeId); const styles = this.styles(this.props.themeId);
const theme = themeStyle(this.props.themeId); const theme = themeStyle(this.props.themeId);
const labelComp = <label style={{ ...theme.textStyle, ...theme.controlBoxLabel }}>{this.formatLabel(key)}</label>; const labelText = this.formatLabel(key);
const labelComp = <label role='rowheader' style={{ ...theme.textStyle, ...theme.controlBoxLabel }}>{labelText}</label>;
let controlComp = null; let controlComp = null;
let editComp = null; let editComp = null;
let editCompHandler = null; let editCompHandler = null;
let editCompIcon = null; let editCompIcon = null;
let editComDescription = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onKeyDown = (event: any) => { const onKeyDown = (event: any) => {
@ -320,6 +322,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
void this.saveProperty(); void this.saveProperty();
}; };
editCompIcon = 'fa-save'; editCompIcon = 'fa-save';
editComDescription = _('Save changes');
} else { } else {
controlComp = ( controlComp = (
<input <input
@ -374,28 +377,35 @@ class NotePropertiesDialog extends React.Component<Props, State> {
this.editPropertyButtonClick(key, value); this.editPropertyButtonClick(key, value);
}; };
editCompIcon = 'fa-edit'; editCompIcon = 'fa-edit';
editComDescription = _('Edit');
} }
// Add the copy icon and the 'copy on click' event // Add the copy icon and the 'copy on click' event
if (key === 'id') { if (key === 'id') {
editCompIcon = 'fa-copy'; editCompIcon = 'fa-copy';
editCompHandler = () => clipboard.writeText(value); editCompHandler = () => clipboard.writeText(value);
editComDescription = _('Copy');
} }
} }
if (editCompHandler && !this.isReadOnly()) { if (editCompHandler && !this.isReadOnly()) {
editComp = ( editComp = (
<a href="#" onClick={editCompHandler} style={styles.editPropertyButton}> <a
href="#"
onClick={editCompHandler}
style={styles.editPropertyButton}
aria-label={editComDescription}
title={editComDescription}
>
<i className={`fas ${editCompIcon}`} aria-hidden="true"></i> <i className={`fas ${editCompIcon}`} aria-hidden="true"></i>
</a> </a>
); );
} }
return ( return (
<div key={key} style={theme.controlBox} className="note-property-box"> <div role='row' key={key} style={theme.controlBox} className="note-property-box">
{labelComp} {labelComp}
{controlComp} <span role='cell'>{controlComp} {editComp}</span>
{editComp}
</div> </div>
); );
} }
@ -439,8 +449,10 @@ class NotePropertiesDialog extends React.Component<Props, State> {
return ( return (
<div style={theme.dialogModalLayer}> <div style={theme.dialogModalLayer}>
<div style={theme.dialogBox}> <div style={theme.dialogBox}>
<div style={theme.dialogTitle}>{_('Note properties')}</div> <div style={theme.dialogTitle} id='note-properties-dialog-title'>{_('Note properties')}</div>
<div>{noteComps}</div> <div role='table' aria-labelledby='note-properties-dialog-title'>
{noteComps}
</div>
<DialogButtonRow themeId={this.props.themeId} okButtonShow={!this.isReadOnly()} okButtonRef={this.okButton} onClick={this.buttonRow_click}/> <DialogButtonRow themeId={this.props.themeId} okButtonShow={!this.isReadOnly()} okButtonRef={this.okButton} onClick={this.buttonRow_click}/>
</div> </div>
</div> </div>

View File

@ -2,12 +2,14 @@ import * as React from 'react';
import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types'; import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
import ExpandLink from './ExpandLink'; import ExpandLink from './ExpandLink';
import { StyledListItem, StyledListItemAnchor, StyledNoteCount, StyledShareIcon, StyledSpanFix } from '../styles'; import { StyledListItem, StyledListItemAnchor, StyledShareIcon, StyledSpanFix } from '../styles';
import { ItemClickListener, ItemContextMenuListener, ItemDragListener } from '../types'; import { ItemClickListener, ItemContextMenuListener, ItemDragListener } from '../types';
import FolderIconBox from '../../FolderIconBox'; import FolderIconBox from '../../FolderIconBox';
import { getTrashFolderIcon, getTrashFolderId } from '@joplin/lib/services/trash'; import { getTrashFolderIcon, getTrashFolderId } from '@joplin/lib/services/trash';
import Folder from '@joplin/lib/models/Folder'; import Folder from '@joplin/lib/models/Folder';
import { ModelType } from '@joplin/lib/BaseModel'; import { ModelType } from '@joplin/lib/BaseModel';
import { _ } from '@joplin/lib/locale';
import NoteCount from './NoteCount';
const renderFolderIcon = (folderIcon: FolderIcon) => { const renderFolderIcon = (folderIcon: FolderIcon) => {
if (!folderIcon) { if (!folderIcon) {
@ -47,8 +49,8 @@ interface FolderItemProps {
function FolderItem(props: FolderItemProps) { function FolderItem(props: FolderItemProps) {
const { hasChildren, showFolderIcon, isExpanded, parentId, depth, selected, folderId, folderTitle, folderIcon, noteCount, onFolderDragStart_, onFolderDragOver_, onFolderDrop_, itemContextMenu, folderItem_click, onFolderToggleClick_, shareId } = props; const { hasChildren, showFolderIcon, isExpanded, parentId, depth, selected, folderId, folderTitle, folderIcon, noteCount, onFolderDragStart_, onFolderDragOver_, onFolderDrop_, itemContextMenu, folderItem_click, onFolderToggleClick_, shareId } = props;
const noteCountComp = noteCount ? <StyledNoteCount className="note-count-label">{noteCount}</StyledNoteCount> : null; const shareTitle = _('Shared');
const shareIcon = shareId && !parentId ? <StyledShareIcon className="fas fa-share-alt"></StyledShareIcon> : null; const shareIcon = shareId && !parentId ? <StyledShareIcon aria-label={shareTitle} title={shareTitle} className="fas fa-share-alt"/> : null;
const draggable = ![getTrashFolderId(), Folder.conflictFolderId()].includes(folderId); const draggable = ![getTrashFolderId(), Folder.conflictFolderId()].includes(folderId);
const doRenderFolderIcon = () => { const doRenderFolderIcon = () => {
@ -69,6 +71,7 @@ function FolderItem(props: FolderItemProps) {
isConflictFolder={folderId === Folder.conflictFolderId()} isConflictFolder={folderId === Folder.conflictFolderId()}
href="#" href="#"
selected={selected} selected={selected}
aria-selected={selected}
shareId={shareId} shareId={shareId}
data-id={folderId} data-id={folderId}
data-type={ModelType.Folder} data-type={ModelType.Folder}
@ -80,7 +83,7 @@ function FolderItem(props: FolderItemProps) {
onDoubleClick={onFolderToggleClick_} onDoubleClick={onFolderToggleClick_}
> >
{doRenderFolderIcon()}<StyledSpanFix className="title">{folderTitle}</StyledSpanFix> {doRenderFolderIcon()}<StyledSpanFix className="title">{folderTitle}</StyledSpanFix>
{shareIcon} {noteCountComp} {shareIcon} <NoteCount count={noteCount}/>
</StyledListItemAnchor> </StyledListItemAnchor>
</StyledListItem> </StyledListItem>
); );

View File

@ -61,7 +61,7 @@ const HeaderItem: React.FC<Props> = props => {
tabIndex={0} tabIndex={0}
ref={props.anchorRef} ref={props.anchorRef}
> >
<StyledHeaderIcon className={item.iconName}/> <StyledHeaderIcon aria-label='' className={item.iconName}/>
<StyledHeaderLabel>{item.label}</StyledHeaderLabel> <StyledHeaderLabel>{item.label}</StyledHeaderLabel>
</StyledHeader> </StyledHeader>
{ item.onPlusButtonClick && addButton } { item.onPlusButtonClick && addButton }

View File

@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { StyledNoteCount } from '../styles'; import { _n } from '@joplin/lib/locale';
interface Props { interface Props {
@ -8,7 +8,8 @@ interface Props {
const NoteCount: React.FC<Props> = props => { const NoteCount: React.FC<Props> = props => {
const count = props.count; const count = props.count;
return count ? <StyledNoteCount className="note-count-label">{count}</StyledNoteCount> : null; const title = _n('Contains %d note', 'Contains %d notes', count, count);
return count ? <div role='note' aria-label={title} title={title} className="note-count-label">{count}</div> : null;
}; };
export default NoteCount; export default NoteCount;

View File

@ -33,10 +33,12 @@ const TagItem = (props: Props) => {
}, [props.onClick, tag]); }, [props.onClick, tag]);
return ( return (
<StyledListItem selected={selected} <StyledListItem
selected={selected}
className={`list-item-container ${selected ? 'selected' : ''}`} className={`list-item-container ${selected ? 'selected' : ''}`}
onDrop={props.onTagDrop} onDrop={props.onTagDrop}
data-tag-id={tag.id} data-tag-id={tag.id}
aria-selected={selected}
> >
<EmptyExpandLink/> <EmptyExpandLink/>
<StyledListItemAnchor <StyledListItemAnchor

View File

@ -1,4 +1,5 @@
@use 'styles/folder-and-tag-list.scss'; @use 'styles/folder-and-tag-list.scss';
@use 'styles/note-count-label.scss';
@use 'styles/sidebar-expand-icon.scss'; @use 'styles/sidebar-expand-icon.scss';
@use 'styles/sidebar-expand-link.scss'; @use 'styles/sidebar-expand-link.scss';
@use 'styles/sidebar-header-container.scss'; @use 'styles/sidebar-header-container.scss';

View File

@ -95,12 +95,6 @@ export const StyledShareIcon = styled.i`
margin-left: 8px; margin-left: 8px;
`; `;
export const StyledNoteCount = styled.div`
color: ${(props: StyleProps) => props.theme.colorFaded2};
padding-left: 8px;
user-select: none;
`;
export const StyledSynchronizeButton = styled(Button)` export const StyledSynchronizeButton = styled(Button)`
width: 100%; width: 100%;
`; `;

View File

@ -0,0 +1,6 @@
.note-count-label {
color: var(--joplin-color-faded2);
padding-left: 8px;
user-select: none;
}

View File

@ -50,16 +50,22 @@ export default function ToolbarButton(props: Props) {
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis' }; textOverflow: 'ellipsis' };
const disabled = !isEnabled;
return ( return (
<StyledRoot <StyledRoot
className={classes.join(' ')} className={classes.join(' ')}
disabled={!isEnabled}
title={tooltip} title={tooltip}
href="#" href="#"
hasTitle={!!title} hasTitle={!!title}
onClick={() => { onClick={() => {
if (isEnabled && onClick) onClick(); if (isEnabled && onClick) onClick();
}} }}
// At least on MacOS, the disabled HTML prop isn't sufficient for the screen reader
// to read the element as disable. For this, aria-disabled is necessary.
disabled={disabled}
aria-disabled={!isEnabled}
role='button'
> >
{icon} {icon}
<span style={style}>{title}</span> <span style={style}>{title}</span>

View File

@ -59,6 +59,7 @@ export interface OnChangeEvent {
export default function(props: Props) { export default function(props: Props) {
const iconName = !props.searchStarted ? CommandService.instance().iconName('search') : 'fa fa-times'; const iconName = !props.searchStarted ? CommandService.instance().iconName('search') : 'fa fa-times';
const iconLabel = !props.searchStarted ? _('Search') : _('Clear search');
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onChange = useCallback((event: any) => { const onChange = useCallback((event: any) => {
@ -79,7 +80,10 @@ export default function(props: Props) {
spellCheck={false} spellCheck={false}
disabled={props.disabled} disabled={props.disabled}
/> />
<SearchButton onClick={props.onSearchButtonClick}> <SearchButton
aria-label={iconLabel}
onClick={props.onSearchButtonClick}
>
<SearchButtonIcon className={iconName}/> <SearchButtonIcon className={iconName}/>
</SearchButton> </SearchButton>
</Root> </Root>

View File

@ -2,6 +2,7 @@
<html> <html>
<head id="joplin-container-root-head"> <head id="joplin-container-root-head">
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Note viewer</title>
<style> <style>
body { body {