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:
parent
62b4add780
commit
bd08f01b8c
1 changed files with 118 additions and 24 deletions
|
@ -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
|
||||||
|
className={className}
|
||||||
|
onClick={() => {
|
||||||
if (url) {
|
if (url) {
|
||||||
window.location.href = 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue