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

Inline Help Search Accessibility #43963

Merged
merged 4 commits into from
Jul 15, 2020
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
70 changes: 2 additions & 68 deletions client/blocks/inline-help/inline-help-search-card.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import PropTypes from 'prop-types';
import React, { useRef, useEffect } from 'react';
import { connect } from 'react-redux';
import { localize } from 'i18n-calypso';
import { identity, includes } from 'lodash';
import page from 'page';
import { identity } from 'lodash';
import debugFactory from 'debug';

/**
Expand All @@ -15,28 +14,16 @@ import debugFactory from 'debug';
import { recordTracksEvent } from 'state/analytics/actions';
import SearchCard from 'components/search-card';
import getInlineHelpCurrentlySelectedLink from 'state/inline-help/selectors/get-inline-help-currently-selected-link';
import getSelectedResultIndex from 'state/inline-help/selectors/get-selected-result-index';
import isRequestingInlineHelpSearchResultsForQuery from 'state/inline-help/selectors/is-requesting-inline-help-search-results-for-query';
import getInlineHelpCurrentlySelectedResult from 'state/inline-help/selectors/get-inline-help-currently-selected-result';
import {
setInlineHelpSearchQuery,
selectNextResult,
selectPreviousResult,
} from 'state/inline-help/actions';
import { SUPPORT_TYPE_ADMIN_SECTION } from './constants';
import { setInlineHelpSearchQuery } from 'state/inline-help/actions';

/**
* Module variables
*/
const debug = debugFactory( 'calypso:inline-help' );

const InlineHelpSearchCard = ( {
setPreviousResult,
setNextResult,
selectedResultIndex,
selectedResult = {},
query = '',
onSelect,
track,
location = 'inline-help-popover',
setSearchQuery,
Expand All @@ -59,52 +46,6 @@ const InlineHelpSearchCard = ( {
return () => window.clearTimeout( timerId );
}, [ cardRef, location ] );

const onKeyDown = ( event ) => {
// ignore keyboard access when manipulating a text selection in input etc.
if ( event.getModifierState( 'Shift' ) ) {
return;
}
// take over control if and only if it's one of our keys
if ( includes( [ 'ArrowUp', 'ArrowDown', 'Enter' ], event.key ) ) {
event.preventDefault();
} else {
return;
}

switch ( event.key ) {
case 'ArrowUp':
setPreviousResult();
break;
case 'ArrowDown':
setNextResult();
break;
case 'Enter': {
cpapazoglou marked this conversation as resolved.
Show resolved Hide resolved
// check and catch admin section links.
const { support_type: supportType, link } = selectedResult;
if ( supportType === SUPPORT_TYPE_ADMIN_SECTION && link ) {
track( 'calypso_inlinehelp_admin_section_visit', {
link: link,
search_term: query,
} );

// push state only if it's internal link.
if ( ! /^http/.test( link ) ) {
event.preventDefault();
page( link );
} else {
// redirect external links.
window.location.href = link;
}

return;
}

selectedResultIndex >= 0 && onSelect( event );
break;
}
}
};

const searchHelperHandler = ( searchQuery ) => {
const inputQuery = searchQuery.trim();

Expand All @@ -126,34 +67,27 @@ const InlineHelpSearchCard = ( {
searching={ isSearching }
initialValue={ query }
onSearch={ searchHelperHandler }
onKeyDown={ onKeyDown }
placeholder={ placeholder || translate( 'Search for help…' ) }
delaySearch={ true }
/>
);
};

InlineHelpSearchCard.propTypes = {
onSelect: PropTypes.func.isRequired,
translate: PropTypes.func,
track: PropTypes.func,
query: PropTypes.string,
placeholder: PropTypes.string,
location: PropTypes.string,
selectedResult: PropTypes.object,
};

const mapStateToProps = ( state, ownProps ) => ( {
isSearching: isRequestingInlineHelpSearchResultsForQuery( state, ownProps.query ),
selectedLink: getInlineHelpCurrentlySelectedLink( state ),
selectedResultIndex: getSelectedResultIndex( state ),
selectedResult: getInlineHelpCurrentlySelectedResult( state ),
} );
const mapDispatchToProps = {
track: recordTracksEvent,
setSearchQuery: setInlineHelpSearchQuery,
setNextResult: selectNextResult,
setPreviousResult: selectPreviousResult,
};

export default connect( mapStateToProps, mapDispatchToProps )( localize( InlineHelpSearchCard ) );
119 changes: 70 additions & 49 deletions client/blocks/inline-help/inline-help-search-results.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import React, { useRef, Fragment } from 'react';
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { identity, isEmpty, noop } from 'lodash';
import { connect } from 'react-redux';
Expand Down Expand Up @@ -37,20 +37,13 @@ function HelpSearchResults( {
onAdminSectionSelect = noop,
searchQuery = '',
searchResults = [],
selectedResult = {},
selectedResultIndex = -1,
selectSearchResult,
translate = identity,
placeholderLines,
track,
} ) {
const supportTypeRef = useRef( searchResults?.[ 0 ]?.support_type );

function getTitleBySectionType( addSection, type, query = '' ) {
if ( ! addSection ) {
return null;
}

function getTitleBySectionType( type, query = '' ) {
let title = '';
switch ( type ) {
case SUPPORT_TYPE_CONTEXTUAL_HELP:
Expand All @@ -70,16 +63,11 @@ function HelpSearchResults( {
return null;
}

return (
<li className="inline-help__results-title">
<h2>{ title }</h2>
</li>
);
return title;
}

const onLinkClickHandler = ( event ) => {
const { support_type: supportType, link } = selectedResult;

const onLinkClickHandler = ( event, result ) => {
const { support_type: supportType, link } = result;
// check and catch admin section links.
if ( supportType === SUPPORT_TYPE_ADMIN_SECTION && link ) {
// record track-event.
Expand All @@ -98,53 +86,84 @@ function HelpSearchResults( {
return;
}

// Set the current selected result item.
onSelect( event );
onSelect( event, result );
};

const selectCurrentResultIndex = ( index ) => () => selectSearchResult( index );

const renderHelpLink = (
{ link, key, description, title, support_type = SUPPORT_TYPE_API_HELP, icon = 'domains' },
index
) => {
const addResultsSection = supportTypeRef?.current !== support_type || ! index;
if ( addResultsSection ) {
supportTypeRef.current = support_type;
}
const renderHelpLink = ( result ) => {
const { link, key, title, support_type = SUPPORT_TYPE_API_HELP, icon = 'domains' } = result;
const resultIndex = searchResults.findIndex( ( r ) => r.link === link );

const classes = classNames( 'inline-help__results-item', {
'is-selected': selectedResultIndex === index,
'is-selected': selectedResultIndex === resultIndex,
} );

return (
<Fragment key={ link ?? key }>
{ getTitleBySectionType( addResultsSection, support_type, searchQuery ) }
<li className={ classes }>
<a
href={ localizeUrl( link ) }
onMouseDown={ selectCurrentResultIndex( index ) }
onClick={ onLinkClickHandler }
title={ decodeEntities( description ) }
tabIndex={ -1 }
>
{ support_type === SUPPORT_TYPE_ADMIN_SECTION && (
<Gridicon icon={ icon } size={ 18 } />
) }
<span>{ preventWidows( decodeEntities( title ) ) }</span>
</a>
<div className="inline-help__results-cell">
<a
href={ localizeUrl( link ) }
onClick={ ( event ) => {
event.preventDefault();
selectSearchResult( resultIndex );
onLinkClickHandler( event, result );
} }
>
{ support_type === SUPPORT_TYPE_ADMIN_SECTION && (
<Gridicon icon={ icon } size={ 18 } />
) }
<span>{ preventWidows( decodeEntities( title ) ) }</span>
</a>
</div>
</li>
</Fragment>
);
};

const renderSearchResultsSection = ( id, title, results ) => {
/* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */
return (
<Fragment key={ id }>
{ title ? (
<h3 id={ id } className="inline-help__results-title">
{ title }
</h3>
) : null }
<ul className="inline-help__results-list" aria-labelledby={ title ? id : undefined }>
{ results.map( renderHelpLink ) }
</ul>
</Fragment>
);
/* eslint-enable jsx-a11y/no-noninteractive-element-to-interactive-role */
};

const renderSearchSections = ( results, query ) => {
// Get the unique result types
// TODO: Clean this up. There has to be a simpler way to find the unique search result types
const searchResultTypes = results
.map( ( searchResult ) => searchResult.support_type )
.filter( ( type, index, arr ) => arr.indexOf( type ) === index );

return searchResultTypes.map( ( resultType ) => {
return renderSearchResultsSection(
`inline-search--${ resultType }`,
getTitleBySectionType( resultType, query ),
results.filter( ( r ) => r.support_type === resultType )
);
} );
};

const renderSearchResults = () => {
if ( isSearching && ! searchResults.length ) {
// reset current section reference.
supportTypeRef.current = null;

// search, but no results so far
return <PlaceholderLines lines={ placeholderLines } />;
return (
<>
<div className="inline-help__visually-hidden">
{ translate( 'Loading search results' ) }
</div>
<PlaceholderLines lines={ placeholderLines } />
</>
);
}

return (
Expand All @@ -157,15 +176,17 @@ function HelpSearchResults( {
</p>
) }

<ul className="inline-help__results-list">{ searchResults.map( renderHelpLink ) }</ul>
<div className="inline-help__results" aria-label={ translate( 'Search Results' ) }>
{ renderSearchSections( searchResults, searchQuery ) }
</div>
</>
);
};

return (
<>
<QueryInlineHelpSearch query={ searchQuery } />
{ renderSearchResults() }
<div aria-live="polite">{ renderSearchResults() }</div>
</>
);
}
Expand Down
6 changes: 3 additions & 3 deletions client/blocks/inline-help/placeholder-lines.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import React from 'react';
import { times } from 'lodash';

export default ( { lines = 4 } ) => (
<ul className="inline-help__results-placeholder">
<div className="inline-help__results-placeholder">
{ times( lines, ( n ) => (
<li key={ `line-${ n }` } className="inline-help__results-placeholder-item" />
<div key={ `line-${ n }` } className="inline-help__results-placeholder-item" />
) ) }
</ul>
</div>
);
Loading