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');
|
const { connect } = require('react-redux');
|
||||||
import { themeStyle } from '@joplin/lib/theme';
|
import { themeStyle } from '@joplin/lib/theme';
|
||||||
import { AppState } from '../app.reducer';
|
import { AppState } from '../app.reducer';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tip: string;
|
tip: string;
|
||||||
@ -10,6 +11,9 @@ interface Props {
|
|||||||
themeId: number;
|
themeId: number;
|
||||||
// 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
|
||||||
style: any;
|
style: any;
|
||||||
|
|
||||||
|
'aria-controls'?: string;
|
||||||
|
'aria-expanded'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class HelpButtonComponent extends React.Component<Props> {
|
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 };
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
const extraProps: any = {};
|
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 (
|
return (
|
||||||
<a href="#" style={style} onClick={this.onClick} {...extraProps}>
|
<button
|
||||||
<i style={helpIconStyle} className={'fa fa-question-circle'}></i>
|
style={style}
|
||||||
</a>
|
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;
|
itemRenderer: (item: ItemType, index: number)=> React.JSX.Element;
|
||||||
className?: string;
|
className?: string;
|
||||||
onItemDrop?: DragEventHandler<HTMLElement>;
|
onItemDrop?: DragEventHandler<HTMLElement>;
|
||||||
|
|
||||||
|
id?: string;
|
||||||
|
role?: string;
|
||||||
|
'aria-label'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -164,7 +168,20 @@ class ItemList<ItemType> extends React.Component<Props<ItemType>, State> {
|
|||||||
if (this.props.className) classes.push(this.props.className);
|
if (this.props.className) classes.push(this.props.className);
|
||||||
|
|
||||||
return (
|
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}
|
{itemComps}
|
||||||
</div>
|
</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 './dialog-modal-layer.scss';
|
||||||
@use './user-webview-dialog.scss';
|
@use './user-webview-dialog.scss';
|
||||||
@use './prompt-dialog.scss';
|
@use './prompt-dialog.scss';
|
||||||
|
@use './flat-button.scss';
|
||||||
|
@use './help-text.scss';
|
||||||
@use './toolbar-button.scss';
|
@use './toolbar-button.scss';
|
||||||
@use './editor-toolbar.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,
|
// result, we use this function - which returns either the item_id, if present,
|
||||||
// or the note ID.
|
// or the note ID.
|
||||||
const getResultId = (result: GotoAnythingSearchResult) => {
|
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 {
|
class GotoAnything {
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// 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,
|
borderBottomColor: theme.dividerColor,
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
},
|
},
|
||||||
help: { ...theme.textStyle, marginBottom: 10 },
|
|
||||||
inputHelpWrapper: { display: 'flex', flexDirection: 'row', alignItems: 'center' },
|
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 theme = themeStyle(this.props.themeId);
|
||||||
const style = this.style();
|
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 rowStyle = isSelected ? style.rowSelected : style.row;
|
||||||
const titleHtml = item.fragments
|
const titleHtml = item.fragments
|
||||||
? `<span style="font-weight: bold; color: ${theme.color};">${item.title}</span>`
|
? `<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 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 pathComp = !item.path ? null : <div style={style.rowPath}>{folderIcon} {item.path}</div>;
|
||||||
const fragmentComp = !fragmentsHtml ? null : <div style={style.rowFragments} dangerouslySetInnerHTML={{ __html: (fragmentsHtml) }}></div>;
|
const fragmentComp = !fragmentsHtml ? null : <div style={style.rowFragments} dangerouslySetInnerHTML={{ __html: (fragmentsHtml) }}></div>;
|
||||||
|
|
||||||
return (
|
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>
|
<div style={style.rowTitle} dangerouslySetInnerHTML={{ __html: titleHtml }}></div>
|
||||||
{fragmentComp}
|
{fragmentComp}
|
||||||
{pathComp}
|
{pathComp}
|
||||||
@ -619,6 +636,9 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<ItemList
|
<ItemList
|
||||||
ref={this.itemListRef}
|
ref={this.itemListRef}
|
||||||
|
id={itemListId}
|
||||||
|
role='listbox'
|
||||||
|
aria-label={_('Search results')}
|
||||||
itemHeight={style.itemHeight}
|
itemHeight={style.itemHeight}
|
||||||
items={this.state.results}
|
items={this.state.results}
|
||||||
style={itemListStyle}
|
style={itemListStyle}
|
||||||
@ -629,14 +649,40 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const style = this.style();
|
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 (
|
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}
|
{helpComp}
|
||||||
<div style={style.inputHelpWrapper}>
|
<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} />
|
<input
|
||||||
<HelpButton onClick={this.helpButton_onClick} />
|
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>
|
</div>
|
||||||
{this.renderList()}
|
{this.renderList()}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
Loading…
Reference in New Issue
Block a user