mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Desktop: Accessibility: Fix screen reader doesn't read Goto Anything search results or help button label (#10816)
This commit is contained in:
parent
1c2c071952
commit
9f997c2fb6
@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
const { connect } = require('react-redux');
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
interface Props {
|
||||
tip: string;
|
||||
@ -10,6 +11,9 @@ interface Props {
|
||||
themeId: number;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
style: any;
|
||||
|
||||
'aria-controls'?: string;
|
||||
'aria-expanded'?: string;
|
||||
}
|
||||
|
||||
class HelpButtonComponent extends React.Component<Props> {
|
||||
@ -29,11 +33,21 @@ class HelpButtonComponent extends React.Component<Props> {
|
||||
const helpIconStyle = { flex: 0, width: 16, height: 16, marginLeft: 10 };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const extraProps: any = {};
|
||||
if (this.props.tip) extraProps['data-tip'] = this.props.tip;
|
||||
if (this.props.tip) {
|
||||
extraProps['data-tip'] = this.props.tip;
|
||||
extraProps['aria-description'] = this.props.tip;
|
||||
}
|
||||
return (
|
||||
<a href="#" style={style} onClick={this.onClick} {...extraProps}>
|
||||
<i style={helpIconStyle} className={'fa fa-question-circle'}></i>
|
||||
</a>
|
||||
<button
|
||||
style={style}
|
||||
onClick={this.onClick}
|
||||
className='flat-button'
|
||||
aria-controls={this.props['aria-controls']}
|
||||
aria-expanded={this.props['aria-expanded']}
|
||||
{...extraProps}
|
||||
>
|
||||
<i style={helpIconStyle} className={'fa fa-question-circle'} role='img' aria-label={_('Help')}></i>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,10 @@ interface Props<ItemType> {
|
||||
itemRenderer: (item: ItemType, index: number)=> React.JSX.Element;
|
||||
className?: string;
|
||||
onItemDrop?: DragEventHandler<HTMLElement>;
|
||||
|
||||
id?: string;
|
||||
role?: string;
|
||||
'aria-label'?: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@ -164,7 +168,20 @@ class ItemList<ItemType> extends React.Component<Props<ItemType>, State> {
|
||||
if (this.props.className) classes.push(this.props.className);
|
||||
|
||||
return (
|
||||
<div ref={this.listRef} className={classes.join(' ')} style={style} onScroll={this.onScroll} onKeyDown={this.onKeyDown} onDrop={this.onDrop}>
|
||||
<div
|
||||
ref={this.listRef}
|
||||
className={classes.join(' ')}
|
||||
style={style}
|
||||
|
||||
id={this.props.id}
|
||||
role={this.props.role}
|
||||
aria-label={this.props['aria-label']}
|
||||
aria-setsize={items.length}
|
||||
|
||||
onScroll={this.onScroll}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onDrop={this.onDrop}
|
||||
>
|
||||
{itemComps}
|
||||
</div>
|
||||
);
|
||||
|
7
packages/app-desktop/gui/styles/flat-button.scss
Normal file
7
packages/app-desktop/gui/styles/flat-button.scss
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
.flat-button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
}
|
12
packages/app-desktop/gui/styles/help-text.scss
Normal file
12
packages/app-desktop/gui/styles/help-text.scss
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
.help-text {
|
||||
color: var(--joplin-color);
|
||||
line-height: var(--joplin-line-height);
|
||||
font-family: var(--joplin-font-family);
|
||||
font-size: var(--joplin-font-size);
|
||||
margin-bottom: 10px;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -2,5 +2,7 @@
|
||||
@use './dialog-modal-layer.scss';
|
||||
@use './user-webview-dialog.scss';
|
||||
@use './prompt-dialog.scss';
|
||||
@use './flat-button.scss';
|
||||
@use './help-text.scss';
|
||||
@use './toolbar-button.scss';
|
||||
@use './editor-toolbar.scss';
|
||||
|
@ -93,9 +93,13 @@ const getContentMarkupLanguageAndBody = (result: GotoAnythingSearchResult, notes
|
||||
// result, we use this function - which returns either the item_id, if present,
|
||||
// or the note ID.
|
||||
const getResultId = (result: GotoAnythingSearchResult) => {
|
||||
return result.item_id ? result.item_id : result.id;
|
||||
// This ID used as a DOM ID for accessibility purposes, so it is prefixed to prevent
|
||||
// name collisions.
|
||||
return `goto-anything-result-${result.item_id ? result.item_id : result.id}`;
|
||||
};
|
||||
|
||||
const itemListId = 'goto-anything-item-list';
|
||||
|
||||
class GotoAnything {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
@ -192,7 +196,6 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
borderBottomColor: theme.dividerColor,
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
help: { ...theme.textStyle, marginBottom: 10 },
|
||||
inputHelpWrapper: { display: 'flex', flexDirection: 'row', alignItems: 'center' },
|
||||
};
|
||||
|
||||
@ -530,10 +533,11 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
public renderItem(item: GotoAnythingSearchResult) {
|
||||
public renderItem(item: GotoAnythingSearchResult, index: number) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const style = this.style();
|
||||
const isSelected = getResultId(item) === this.state.selectedItemId;
|
||||
const resultId = getResultId(item);
|
||||
const isSelected = resultId === this.state.selectedItemId;
|
||||
const rowStyle = isSelected ? style.rowSelected : style.row;
|
||||
const titleHtml = item.fragments
|
||||
? `<span style="font-weight: bold; color: ${theme.color};">${item.title}</span>`
|
||||
@ -541,12 +545,25 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
|
||||
const fragmentsHtml = !item.fragments ? null : surroundKeywords(this.state.keywords, item.fragments, `<span style="color: ${theme.searchMarkerColor}; background-color: ${theme.searchMarkerBackgroundColor}">`, '</span>', { escapeHtml: true });
|
||||
|
||||
const folderIcon = <i style={{ fontSize: theme.fontSize, marginRight: 2 }} className="fa fa-book" />;
|
||||
const folderIcon = <i style={{ fontSize: theme.fontSize, marginRight: 2 }} className="fa fa-book" role='img' aria-label={_('Notebook')} />;
|
||||
const pathComp = !item.path ? null : <div style={style.rowPath}>{folderIcon} {item.path}</div>;
|
||||
const fragmentComp = !fragmentsHtml ? null : <div style={style.rowFragments} dangerouslySetInnerHTML={{ __html: (fragmentsHtml) }}></div>;
|
||||
|
||||
return (
|
||||
<div key={getResultId(item)} className={isSelected ? 'selected' : null} style={rowStyle} onClick={this.listItem_onClick} data-id={item.id} data-parent-id={item.parent_id} data-type={item.type}>
|
||||
<div
|
||||
key={resultId}
|
||||
className={isSelected ? 'selected' : null}
|
||||
style={rowStyle}
|
||||
onClick={this.listItem_onClick}
|
||||
|
||||
data-id={item.id}
|
||||
data-parent-id={item.parent_id}
|
||||
data-type={item.type}
|
||||
|
||||
role='option'
|
||||
id={resultId}
|
||||
aria-posinset={index + 1}
|
||||
>
|
||||
<div style={style.rowTitle} dangerouslySetInnerHTML={{ __html: titleHtml }}></div>
|
||||
{fragmentComp}
|
||||
{pathComp}
|
||||
@ -619,6 +636,9 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
return (
|
||||
<ItemList
|
||||
ref={this.itemListRef}
|
||||
id={itemListId}
|
||||
role='listbox'
|
||||
aria-label={_('Search results')}
|
||||
itemHeight={style.itemHeight}
|
||||
items={this.state.results}
|
||||
style={itemListStyle}
|
||||
@ -629,14 +649,40 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
|
||||
public render() {
|
||||
const style = this.style();
|
||||
const helpComp = !this.state.showHelp ? null : <div className="help-text" style={style.help}>{_('Type a note title or part of its content to jump to it. Or type # followed by a tag name, or @ followed by a notebook name. Or type : to search for commands.')}</div>;
|
||||
const helpTextId = 'goto-anything-help-text';
|
||||
const helpComp = (
|
||||
<div
|
||||
className='help-text'
|
||||
aria-live='polite'
|
||||
id={helpTextId}
|
||||
style={style.help}
|
||||
hidden={!this.state.showHelp}
|
||||
>{_('Type a note title or part of its content to jump to it. Or type # followed by a tag name, or @ followed by a notebook name. Or type : to search for commands.')}</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog className="go-to-anything-dialog" onCancel={this.modalLayer_onDismiss} contentStyle={style.dialogBox}>
|
||||
<Dialog className='go-to-anything-dialog' onCancel={this.modalLayer_onDismiss} contentStyle={style.dialogBox}>
|
||||
{helpComp}
|
||||
<div style={style.inputHelpWrapper}>
|
||||
<input autoFocus type="text" style={style.input} ref={this.inputRef} value={this.state.query} onChange={this.input_onChange} onKeyDown={this.input_onKeyDown} />
|
||||
<HelpButton onClick={this.helpButton_onClick} />
|
||||
<input
|
||||
autoFocus
|
||||
type='text'
|
||||
style={style.input}
|
||||
ref={this.inputRef}
|
||||
value={this.state.query}
|
||||
onChange={this.input_onChange}
|
||||
onKeyDown={this.input_onKeyDown}
|
||||
|
||||
aria-describedby={helpTextId}
|
||||
aria-autocomplete='list'
|
||||
aria-controls={itemListId}
|
||||
aria-activedescendant={this.state.selectedItemId}
|
||||
/>
|
||||
<HelpButton
|
||||
onClick={this.helpButton_onClick}
|
||||
aria-controls={helpTextId}
|
||||
aria-expanded={this.state.showHelp}
|
||||
/>
|
||||
</div>
|
||||
{this.renderList()}
|
||||
</Dialog>
|
||||
|
Loading…
Reference in New Issue
Block a user