File "index.js"
Full Path: /home/siazco/grocery.siazco.se/wp-content/plugins/better-wp-security/core/packages/search/src/components/search/index.js
File size: 6.16 KB
MIME-type: text/x-java
Charset: utf-8
/**
* External dependencies
*/
import { Link, useHistory } from 'react-router-dom';
import { createLocation } from 'history';
import { isEmpty, map, noop } from 'lodash';
/**
* WordPress dependencies
*/
import { VisuallyHidden } from '@wordpress/components';
import { __, _n, sprintf } from '@wordpress/i18n';
import {
useState,
useCallback,
useRef,
forwardRef,
useImperativeHandle,
} from '@wordpress/element';
import { useRegistry } from '@wordpress/data';
import {
useInstanceId,
useDebounce,
useKeyboardShortcut,
} from '@wordpress/compose';
import { ENTER, SPACE, DOWN } from '@wordpress/keycodes';
import { speak } from '@wordpress/a11y';
/**
* iThemes dependencies
*/
import { SearchControl } from '@ithemes/ui';
/**
* Internal dependencies
*/
import { useFocusOutside, useMergeRefs } from '@ithemes/security-hocs';
import {
ActiveDescendantContainer,
Markup,
} from '@ithemes/security-components';
import Engine from '../../engine';
import './style.scss';
export default forwardRef( function Search(
{ onPick = noop, showResults },
ref
) {
// UI State
const [ query, setQuery ] = useState( '' );
const [ isSearching, setIsSearching ] = useState( false );
const searchRef = useRef();
const resultsRef = useRef();
// Results handling
const [ results, setResults ] = useState( [] );
const registry = useRegistry();
const search = useCallback(
( searchQuery ) => {
const searchResults = new Engine(
searchQuery,
registry
).getResults();
setResults( searchResults[ 0 ] );
speak(
sprintf(
/* translators: 1. Number of results. */
_n(
'%d result found.',
'%d results found.',
searchResults[ 1 ],
'better-wp-security'
),
searchResults[ 1 ]
)
);
},
[ registry ]
);
const searchDebounced = useDebounce( search, 50 );
// Event handlers
const onChange = ( nextQuery ) => {
setQuery( nextQuery );
searchDebounced( nextQuery );
};
const onKeyDown = ( e ) => {
if ( e.keyCode === DOWN ) {
e.preventDefault();
resultsRef.current.focus();
}
};
const onSlash = useCallback(
( e ) => {
if ( searchRef.current ) {
e.preventDefault();
searchRef.current.focus();
}
},
[ searchRef ]
);
useKeyboardShortcut( '/', onSlash );
return (
<div
className="itsec-search"
{ ...useFocusOutside( () => setIsSearching( false ) ) }
>
<div>
<SearchControl
value={ query }
onChange={ onChange }
onFocus={ () => setIsSearching( true ) }
onKeyDown={ onKeyDown }
ref={ useMergeRefs( [ ref, searchRef ] ) }
placeholder={ __( 'Search for features, settings, and more', 'better-wp-security' ) }
omitSeparators
size="large"
/>
{ ( isSearching || showResults ) && query.length >= 3 && (
<SearchResults
results={ results }
exitSearch={ ( result ) => {
onPick( result );
setIsSearching( false );
} }
ref={ resultsRef }
onPick={ onPick }
/>
) }
</div>
</div>
);
} );
const SearchResults = forwardRef( function(
{ results, exitSearch, onPick },
ref
) {
const containerRef = useRef();
useImperativeHandle( ref, () => ( {
focus() {
containerRef.current.focus();
},
} ) );
const id = useInstanceId( SearchResults, 'itsec-search' );
const idPrefix = id + '__result__';
const navigateTo = useNavigateTo();
const [ active, setActive ] = useState( '' );
const onKeyDown = ( { keyCode } ) => {
if ( active && ( keyCode === ENTER || keyCode === SPACE ) ) {
onPick( active );
navigateTo( active );
exitSearch();
}
};
const onFocus = () => {
if ( ! active && ! isEmpty( results ) ) {
const [ , firstKind ] = Object.entries( results )[ 0 ];
if ( firstKind.items?.length ) {
setActive( firstKind.items[ 0 ].route );
} else if ( ! isEmpty( firstKind.groups ) ) {
const [ , firstGroup ] = Object.entries(
firstKind.groups
)[ 0 ];
setActive( firstGroup.items[ 0 ].route );
}
}
};
if ( isEmpty( results ) ) {
return null;
}
return (
<>
<VisuallyHidden id={ id + '__label' }>
{ __( 'Search Results', 'better-wp-security' ) }
</VisuallyHidden>
<ActiveDescendantContainer
className="itsec-search__results"
id={ id }
active={ active && idPrefix + active }
onNavigate={ ( result ) =>
setActive( result.substring( idPrefix.length ) )
}
onKeyDown={ onKeyDown }
onFocus={ onFocus }
role="listbox"
descendantRoles="option"
ref={ containerRef }
aria-labelledby={ id + '__label' }
>
{ map( results, ( kind, slug ) => (
<KindResults
key={ slug }
{ ...kind }
active={ active }
idPrefix={ idPrefix }
exitSearch={ exitSearch }
/>
) ) }
</ActiveDescendantContainer>
</>
);
} );
function KindResults( { title, items, groups, ...rest } ) {
const id = useInstanceId( SearchResults, 'itsec-search__kind' );
return (
<ul className="itsec-search__kind" role="group" aria-labelledby={ id }>
<li role="presentation" id={ id }>
{ title }
</li>
{ ( items || [] ).map( ( item ) => (
<Result key={ item.route } { ...item } { ...rest } />
) ) }
{ map( groups, ( group, slug ) => (
<GroupResults key={ slug } { ...group } { ...rest } />
) ) }
</ul>
);
}
function GroupResults( { title, items, ...rest } ) {
const id = useInstanceId( SearchResults, 'itsec-search__group' );
return (
<ul className="itsec-search__group" role="group" aria-labelledby={ id }>
<li role="presentation" id={ id }>
<span>{ title }</span>
</li>
{ ( items || [] ).map( ( item ) => (
<Result key={ item.route } { ...item } { ...rest } />
) ) }
</ul>
);
}
function Result( { title, description, route, active, idPrefix, exitSearch } ) {
return (
<li
className="itsec-search__result"
role="option"
aria-selected={ active === route ? true : undefined }
id={ idPrefix + route }
aria-label={ title }
>
<Link
to={ route }
tabIndex={ -1 }
onClick={ () => exitSearch( route ) }
>
<span>{ title }</span>
<Markup content={ description } noHtml tagName="p" />
</Link>
</li>
);
}
function useNavigateTo() {
const history = useHistory();
return ( route, mode = 'push' ) =>
history[ mode ]( createLocation( route ) );
}