0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-24 23:48:13 -05:00

Added keyboard navigation for search results

refs https://github.com/TryGhost/Team/issues/1665

- adds keyboard navigation for search results using arrow up/down
This commit is contained in:
Rishabh 2022-07-06 17:21:58 +02:00
parent 62b4add780
commit bd08f01b8c

View file

@ -2,7 +2,7 @@ import Frame from './Frame';
import AppContext from '../AppContext'; import AppContext from '../AppContext';
import {ReactComponent as SearchIcon} from '../icons/search.svg'; import {ReactComponent as SearchIcon} from '../icons/search.svg';
import {ReactComponent as CloseIcon} from '../icons/close.svg'; import {ReactComponent as CloseIcon} from '../icons/close.svg';
import {useContext, useEffect, useRef} from 'react'; import {useContext, useEffect, useMemo, useRef, useState} from 'react';
import {getBundledCssLink} from '../utils/helpers'; import {getBundledCssLink} from '../utils/helpers';
const React = require('react'); const React = require('react');
@ -147,6 +147,11 @@ function SearchBox() {
searchValue: e.target.value searchValue: e.target.value
}); });
}} }}
onKeyDown={(e) => {
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault();
}
}}
className='grow -my-5 py-5 -ml-3 pl-3 text-[1.65rem] focus-visible:outline-none placeholder:text-gray-400' className='grow -my-5 py-5 -ml-3 pl-3 text-[1.65rem] focus-visible:outline-none placeholder:text-gray-400'
placeholder='Search posts, tags, authors..' placeholder='Search posts, tags, authors..'
/> />
@ -175,16 +180,23 @@ function ClearButton() {
); );
} }
function TagListItem({tag}) { function TagListItem({tag, selectedResult, setSelectedResult}) {
const {name, url} = tag; const {name, url, id} = tag;
let className = 'flex items-center py-3 -mx-4 sm:-mx-7 px-4 sm:px-7 cursor-pointer';
if (id === selectedResult) {
className += ' bg-neutral-100';
}
return ( return (
<div <div
className='flex items-center py-3 -mx-4 sm:-mx-7 px-4 sm:px-7 hover:bg-gray-100 cursor-pointer' className={className}
onClick={() => { onClick={() => {
if (url) { if (url) {
window.location.href = url; window.location.href = url;
} }
}} }}
onMouseEnter={() => {
setSelectedResult(id);
}}
> >
<p className='mr-2 text-sm font-bold text-neutral-400'>#</p> <p className='mr-2 text-sm font-bold text-neutral-400'>#</p>
<h2 className='text-[1.65rem] font-medium leading-tight text-neutral-900 truncate'>{name}</h2> <h2 className='text-[1.65rem] font-medium leading-tight text-neutral-900 truncate'>{name}</h2>
@ -192,7 +204,7 @@ function TagListItem({tag}) {
); );
} }
function TagResults({tags}) { function TagResults({tags, selectedResult, setSelectedResult}) {
if (!tags?.length) { if (!tags?.length) {
return null; return null;
} }
@ -202,6 +214,7 @@ function TagResults({tags}) {
<TagListItem <TagListItem
key={d.name} key={d.name}
tag={d} tag={d}
{...{selectedResult, setSelectedResult}}
/> />
); );
}); });
@ -213,29 +226,42 @@ function TagResults({tags}) {
); );
} }
function PostListItem({post}) { function PostListItem({post, selectedResult, setSelectedResult}) {
const {title, excerpt, url} = post; const {title, excerpt, url, id} = post;
let className = 'py-3 -mx-4 sm:-mx-7 px-4 sm:px-7 cursor-pointer';
if (id === selectedResult) {
className += ' bg-neutral-100';
}
return ( return (
<div className='py-3 -mx-4 sm:-mx-7 px-4 sm:px-7 hover:bg-neutral-100 cursor-pointer' onClick={() => { <div
if (url) { className={className}
window.location.href = url; onClick={() => {
} if (url) {
}}> window.location.href = url;
}
}}
onMouseEnter={() => {
setSelectedResult(id);
}}
>
<h2 className='text-[1.65rem] font-medium leading-tight text-neutral-900'>{title}</h2> <h2 className='text-[1.65rem] font-medium leading-tight text-neutral-900'>{title}</h2>
<p className='text-neutral-400 leading-normal text-sm mt-0 mb-0 truncate'>{excerpt}</p> <p className='text-neutral-400 leading-normal text-sm mt-0 mb-0 truncate'>{excerpt}</p>
</div> </div>
); );
} }
function ShowMoreButtom() { function ShowMoreButton() {
return ( return (
<button className='w-full my-3 p-[1rem] border border-neutral-200 hover:border-neutral-300 text-neutral-800 hover:text-black font-semibold rounded transition duration-150 ease hover:ease'> <button
className='w-full my-3 p-[1rem] border border-neutral-200 hover:border-neutral-300 text-neutral-800 hover:text-black font-semibold rounded transition duration-150 ease hover:ease'
>
Show more results Show more results
</button> </button>
); );
} }
function PostResults({posts}) { function PostResults({posts, selectedResult, setSelectedResult}) {
if (!posts?.length) { if (!posts?.length) {
return null; return null;
} }
@ -245,6 +271,7 @@ function PostResults({posts}) {
<PostListItem <PostListItem
key={d.title} key={d.title}
post={d} post={d}
{...{selectedResult, setSelectedResult}}
/> />
); );
}); });
@ -252,21 +279,28 @@ function PostResults({posts}) {
<div className='border-t border-neutral-200 py-3 px-4 sm:px-7'> <div className='border-t border-neutral-200 py-3 px-4 sm:px-7'>
<h1 className='uppercase text-xs text-neutral-400 font-semibold mb-1 tracking-wide'>Posts</h1> <h1 className='uppercase text-xs text-neutral-400 font-semibold mb-1 tracking-wide'>Posts</h1>
{PostItems} {PostItems}
<ShowMoreButtom /> <ShowMoreButton />
</div> </div>
); );
} }
function AuthorListItem({author}) { function AuthorListItem({author, selectedResult, setSelectedResult}) {
const {name, profile_image: profileImage, url} = author; const {name, profile_image: profileImage, url, id} = author;
let className = 'py-[1rem] -mx-4 sm:-mx-7 px-4 sm:px-7 cursor-pointer flex items-center';
if (id === selectedResult) {
className += ' bg-neutral-100';
}
return ( return (
<div <div
className='py-[1rem] -mx-4 sm:-mx-7 px-4 sm:px-7 hover:bg-neutral-100 cursor-pointer flex items-center' className={className}
onClick={() => { onClick={() => {
if (url) { if (url) {
window.location.href = url; window.location.href = url;
} }
}} }}
onMouseEnter={() => {
setSelectedResult(id);
}}
> >
<AuthorAvatar name={name} avatar={profileImage} /> <AuthorAvatar name={name} avatar={profileImage} />
<h2 className='text-[1.65rem] font-medium leading-tight text-neutral-900 truncate'>{name}</h2> <h2 className='text-[1.65rem] font-medium leading-tight text-neutral-900 truncate'>{name}</h2>
@ -287,7 +321,7 @@ function AuthorAvatar({name, avatar}) {
); );
} }
function AuthorResults({authors}) { function AuthorResults({authors, selectedResult, setSelectedResult}) {
if (!authors?.length) { if (!authors?.length) {
return null; return null;
} }
@ -297,6 +331,7 @@ function AuthorResults({authors}) {
<AuthorListItem <AuthorListItem
key={d.name} key={d.name}
author={d} author={d}
{...{selectedResult, setSelectedResult}}
/> />
); );
}); });
@ -346,14 +381,73 @@ function SearchResultBox() {
function Results({posts, authors, tags}) { function Results({posts, authors, tags}) {
const {searchValue} = useContext(AppContext); const {searchValue} = useContext(AppContext);
const allResults = useMemo(() => {
return [
...authors,
...tags,
...posts
];
}, [authors, tags, posts]);
const defaultId = allResults?.[0]?.id || null;
const [selectedResult, setSelectedResult] = useState(defaultId);
const containerRef = useRef(null);
useEffect(() => {
setSelectedResult(allResults?.[0]?.id || null);
}, [allResults]);
useEffect(() => {
let keyUphandler = (event) => {
const selectedResultIdx = allResults.findIndex((d) => {
return d.id === selectedResult;
});
let nextResult = allResults[selectedResultIdx + 1];
let prevResult = allResults[selectedResultIdx - 1];
if (event.key === 'ArrowUp' && prevResult) {
setSelectedResult(prevResult?.id);
} else if (event.key === 'ArrowDown' && nextResult) {
setSelectedResult(nextResult?.id);
}
if (event.key === 'Enter') {
const selectedResultData = allResults.find((d) => {
return d.id === selectedResult;
});
window.location.href = selectedResultData?.url;
}
};
const containeRefNode = containerRef?.current;
containeRefNode?.ownerDocument.removeEventListener('keyup', keyUphandler);
containeRefNode?.ownerDocument.addEventListener('keyup', keyUphandler);
return () => {
containeRefNode?.ownerDocument?.removeEventListener('keyup', keyUphandler);
};
}, [allResults, selectedResult]);
if (!searchValue) { if (!searchValue) {
return null; return null;
} }
return ( return (
<div className='overflow-y-auto max-h-[calc(100vh-212px)] sm:max-h-[70vh] -mt-[1px]'> <div className='overflow-y-auto max-h-[calc(100vh-212px)] sm:max-h-[70vh] -mt-[1px]' ref={containerRef}>
<AuthorResults authors={authors} /> <AuthorResults
<TagResults tags={tags} /> authors={authors}
<PostResults posts={posts} /> selectedResult={selectedResult}
setSelectedResult={setSelectedResult}
/>
<TagResults
tags={tags}
selectedResult={selectedResult}
setSelectedResult={setSelectedResult}
/>
<PostResults
posts={posts}
selectedResult={selectedResult}
setSelectedResult={setSelectedResult}
/>
</div> </div>
); );
} }