Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Desktop: Accessibility: Fix screen reader doesn't read Goto Anything search results or help button label #10816

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions packages/app-desktop/gui/HelpButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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> {
Expand All @@ -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>
);
}
}
Expand Down
19 changes: 18 additions & 1 deletion packages/app-desktop/gui/ItemList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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>
);
Expand Down
7 changes: 7 additions & 0 deletions packages/app-desktop/gui/styles/flat-button.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

.flat-button {
border: none;
background: transparent;
color: inherit;
padding: 0;
}
12 changes: 12 additions & 0 deletions packages/app-desktop/gui/styles/help-text.scss
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions packages/app-desktop/gui/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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';
66 changes: 56 additions & 10 deletions packages/app-desktop/plugins/GotoAnything.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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' },
};

Expand Down Expand Up @@ -530,23 +533,37 @@ 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>`
: surroundKeywords(this.state.keywords, item.title, `<span style="font-weight: bold; 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 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}
Expand Down Expand Up @@ -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}
Expand All @@ -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>
Expand Down
Loading