mirror of https://github.com/laurent22/joplin.git
Desktop: Accessibility: Add missing labels and role information to several controls (#10788)
parent
6d92e982dc
commit
b108bf799d
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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%;
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
.note-count-label {
|
||||||
|
color: var(--joplin-color-faded2);
|
||||||
|
padding-left: 8px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue