From eeb5730932aaf4ac10a860ae6efb5c6ea17345cb Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 29 Apr 2023 20:59:51 +0800 Subject: [PATCH] Filter bar + helper popup for search form --- src/components/link.jsx | 2 +- src/pages/search.css | 58 +++++ src/pages/search.jsx | 458 ++++++++++++++++++++++++++++++++-------- 3 files changed, 433 insertions(+), 85 deletions(-) diff --git a/src/components/link.jsx b/src/components/link.jsx index ba4886d7..8f076763 100644 --- a/src/components/link.jsx +++ b/src/components/link.jsx @@ -19,7 +19,7 @@ const Link = forwardRef((props, ref) => { let hash = (location.hash || '').replace(/^#/, '').trim(); if (hash === '') hash = '/'; const { to, ...restProps } = props; - const isActive = hash === to; + const isActive = decodeURIComponent(hash) === to; return ( span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.search-popover-item:is(:hover, :focus, .focus) > .icon { + opacity: 1; +} diff --git a/src/pages/search.jsx b/src/pages/search.jsx index d6e9a7ac..5b451e3b 100644 --- a/src/pages/search.jsx +++ b/src/pages/search.jsx @@ -1,6 +1,7 @@ import './search.css'; -import { useEffect, useRef, useState } from 'preact/hooks'; +import { forwardRef } from 'preact/compat'; +import { useEffect, useImperativeHandle, useRef, useState } from 'preact/hooks'; import { useParams, useSearchParams } from 'react-router-dom'; import AccountBlock from '../components/account-block'; @@ -18,25 +19,44 @@ function Search(props) { instance: params.instance, }); const [uiState, setUiState] = useState('default'); - const [searchParams, setSearchParams] = useSearchParams(); - const searchFieldRef = useRef(); + const [searchParams] = useSearchParams(); + const searchFormRef = useRef(); const q = props?.query || searchParams.get('q'); - useTitle(q ? `Search: ${q}` : 'Search', `/search`); + const type = props?.type || searchParams.get('type'); + useTitle( + q + ? `Search: ${q}${ + type + ? ` (${ + { + statuses: 'Posts', + accounts: 'Accounts', + hashtags: 'Hashtags', + }[type] + })` + : '' + }` + : 'Search', + `/search`, + ); const [statusResults, setStatusResults] = useState([]); const [accountResults, setAccountResults] = useState([]); const [hashtagResults, setHashtagResults] = useState([]); useEffect(() => { - searchFieldRef.current?.focus?.(); + // searchFieldRef.current?.focus?.(); + // searchFormRef.current?.focus?.(); if (q) { - searchFieldRef.current.value = q; + // searchFieldRef.current.value = q; + searchFormRef.current?.setValue?.(q); setUiState('loading'); (async () => { const results = await masto.v2.search({ q, - limit: 20, + limit: type ? 40 : 5, resolve: authenticated, + type, }); console.log(results); setStatusResults(results.statuses); @@ -45,7 +65,7 @@ function Search(props) { setUiState('default'); })(); } - }, [q, instance]); + }, [q, type, instance]); return (
@@ -55,89 +75,153 @@ function Search(props) {
-
{ - e.preventDefault(); - const { q } = e.target; - if (q.value) { - setSearchParams({ q: q.value }); - } else { - setSearchParams({}); - } - }} - > - { - if (!e.target.value) { - setSearchParams({}); - } - }} - /> -
-
+ +
 
+ {!!q && ( +
+ {!!type && ‹ All} + {[ + { + label: 'Accounts', + type: 'accounts', + to: `/search?q=${q}&type=accounts`, + }, + { + label: 'Hashtags', + type: 'hashtags', + to: `/search?q=${q}&type=hashtags`, + }, + { + label: 'Posts', + type: 'statuses', + to: `/search?q=${q}&type=statuses`, + }, + ] + .sort((a, b) => { + if (a.type === type) return -1; + if (b.type === type) return 1; + return 0; + }) + .map((link) => ( + {link.label} + ))} +
+ )} {!!q && uiState !== 'loading' ? ( <> -

Accounts

- {accountResults.length > 0 ? ( -
    - {accountResults.map((account) => ( -
  • - -
  • - ))} -
- ) : ( -

No accounts found.

+ {(!type || type === 'accounts') && ( + <> + {type !== 'accounts' && ( +

Accounts

+ )} + {accountResults.length > 0 ? ( + <> +
    + {accountResults.map((account) => ( +
  • + +
  • + ))} +
+ {type !== 'accounts' && ( +
+ + See more accounts + +
+ )} + + ) : ( +

No accounts found.

+ )} + )} -

Hashtags

- {hashtagResults.length > 0 ? ( - - ) : ( -

No hashtags found.

+ {(!type || type === 'hashtags') && ( + <> + {type !== 'hashtags' && ( +

Hashtags

+ )} + {hashtagResults.length > 0 ? ( + <> + + {type !== 'hashtags' && ( +
+ + See more hashtags + +
+ )} + + ) : ( +

No hashtags found.

+ )} + )} -

Posts

- {statusResults.length > 0 ? ( -
    - {statusResults.map((status) => ( -
  • - - - -
  • - ))} -
- ) : ( -

No posts found.

+ {(!type || type === 'statuses') && ( + <> + {type !== 'statuses' && ( +

Posts

+ )} + {statusResults.length > 0 ? ( + <> +
    + {statusResults.map((status) => ( +
  • + + + +
  • + ))} +
+ {type !== 'statuses' && ( +
+ + See more posts + +
+ )} + + ) : ( +

No posts found.

+ )} + )} ) : uiState === 'loading' ? ( @@ -156,3 +240,209 @@ function Search(props) { } export default Search; + +const SearchForm = forwardRef((props, ref) => { + const { instance } = api(); + const [searchParams, setSearchParams] = useSearchParams(); + const [searchMenuOpen, setSearchMenuOpen] = useState(false); + const [query, setQuery] = useState(searchParams.q || ''); + const formRef = useRef(null); + + const searchFieldRef = useRef(null); + useImperativeHandle(ref, () => ({ + setValue: (value) => { + setQuery(value); + }, + focus: () => { + searchFieldRef.current.focus(); + }, + })); + + return ( +
{ + e.preventDefault(); + + if (query) { + setSearchParams({ + q: query, + }); + } else { + setSearchParams({}); + } + }} + > + { + if (!e.target.value) { + setSearchParams({}); + } + }} + onInput={(e) => { + setQuery(e.target.value); + setSearchMenuOpen(true); + }} + onFocus={() => { + setSearchMenuOpen(true); + }} + onBlur={() => { + setTimeout(() => { + setSearchMenuOpen(false); + }, 100); + formRef.current + ?.querySelector('.search-popover-item.focus') + ?.classList.remove('focus'); + }} + onKeyDown={(e) => { + const { key } = e; + switch (key) { + case 'Escape': + setSearchMenuOpen(false); + break; + case 'Down': + case 'ArrowDown': + e.preventDefault(); + if (searchMenuOpen) { + const focusItem = formRef.current.querySelector( + '.search-popover-item.focus', + ); + if (focusItem) { + let nextItem = focusItem.nextElementSibling; + while (nextItem && nextItem.hidden) { + nextItem = nextItem.nextElementSibling; + } + if (nextItem) { + nextItem.classList.add('focus'); + const siblings = Array.from( + nextItem.parentElement.children, + ).filter((el) => el !== nextItem); + siblings.forEach((el) => { + el.classList.remove('focus'); + }); + } + } else { + const firstItem = formRef.current.querySelector( + '.search-popover-item', + ); + if (firstItem) { + firstItem.classList.add('focus'); + } + } + } + break; + case 'Up': + case 'ArrowUp': + e.preventDefault(); + if (searchMenuOpen) { + const focusItem = document.querySelector( + '.search-popover-item.focus', + ); + if (focusItem) { + let prevItem = focusItem.previousElementSibling; + while (prevItem && prevItem.hidden) { + prevItem = prevItem.previousElementSibling; + } + if (prevItem) { + prevItem.classList.add('focus'); + const siblings = Array.from( + prevItem.parentElement.children, + ).filter((el) => el !== prevItem); + siblings.forEach((el) => { + el.classList.remove('focus'); + }); + } + } else { + const lastItem = document.querySelector( + '.search-popover-item:last-child', + ); + if (lastItem) { + lastItem.classList.add('focus'); + } + } + } + break; + case 'Enter': + if (searchMenuOpen) { + const focusItem = document.querySelector( + '.search-popover-item.focus', + ); + if (focusItem) { + e.preventDefault(); + focusItem.click(); + } + setSearchMenuOpen(false); + } + break; + } + }} + /> + +
+ ); +});