Fix/app search (#456)

* Auto open search correctly in global search

* Update search input when navigating to previous/next location

* Revert: Navigate to previous page when canceling search

Has to many edge cases and fixing them would require a global history stack which gets saved in the location state.
This then would lead to the apps internal back navigation to be the same as the browsers which is not wanted, since you should not get moved deeper into the apps navigation when clicking the back button

* Correctly autofocus search input

Sometimes the autofocus did not work, since the input did not exist yet by the time the timeout was triggered

* Restore search input open state when navigation to previous/next location

* Restore search input focus when navigating to previous/next location
This commit is contained in:
schroda
2023-11-12 19:04:40 +01:00
committed by GitHub
parent 35c34b6d1b
commit 8d4687428f
3 changed files with 58 additions and 33 deletions

View File

@@ -12,7 +12,7 @@ import { IconButton, Input, Tooltip } from '@mui/material';
import CancelIcon from '@mui/icons-material/Cancel';
import { useQueryParam, StringParam } from 'use-query-params';
import { useTranslation } from 'react-i18next';
import { useBackButton } from '@/util/useBackButton.ts';
import { useLocation, useNavigate } from 'react-router-dom';
interface IProps {
autoOpen?: boolean;
@@ -23,19 +23,33 @@ const defaultProps = {
};
export const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
const { t } = useTranslation();
const handleBack = useBackButton();
const { autoOpen } = props;
const { t } = useTranslation();
const { pathname, search: locationSearch, state: fullLocationState } = useLocation<{ wasSearchOpen?: boolean }>();
const { wasSearchOpen, ...locationState } = fullLocationState ?? {};
const navigate = useNavigate();
const [query, setQuery] = useQueryParam('query', StringParam);
const [searchOpen, setSearchOpen] = useState(!!query);
const inputRef = React.useRef<HTMLInputElement>();
const [searchString, setSearchString] = useState(query ?? '');
const updateSearchOpenState = (open: boolean) => {
setSearchOpen(open);
// try to focus input component since in case of navigating to the previous/next page in the browser history
// the "openSearch" state might not change and thus, won't trigger a focus
if (open) {
inputRef.current?.focus();
}
};
function handleChange(newQuery: string) {
if (newQuery === '') {
handleBack();
return;
}
@@ -44,25 +58,17 @@ export const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
const cancelSearch = () => {
setSearchString('');
setSearchOpen(false);
handleBack();
setQuery(undefined);
updateSearchOpenState(false);
};
const handleBlur = () => {
if (!searchString) setSearchOpen(false);
};
const openSearch = () => {
setSearchOpen(true);
// Put Focus Action at the end of the Callstack so Input actually exists on the dom
setTimeout(() => {
if (inputRef && inputRef.current) inputRef.current.focus();
});
if (!searchString) updateSearchOpenState(false);
};
const handleKeyboardEvent = (e: KeyboardEvent) => {
if (e.code === 'F3' || (e.ctrlKey && e.code === 'KeyF')) {
e.preventDefault();
openSearch();
updateSearchOpenState(true);
return;
}
@@ -73,21 +79,42 @@ export const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
};
useEffect(() => {
if (autoOpen) {
openSearch();
if ((autoOpen && wasSearchOpen === undefined) || (wasSearchOpen && query)) {
updateSearchOpenState(true);
return;
}
}, []);
updateSearchOpenState(false);
}, [autoOpen, pathname]);
useEffect(() => {
if (!searchOpen || !inputRef.current) {
return;
}
inputRef.current.focus();
}, [searchOpen, inputRef.current]);
useEffect(() => {
if (wasSearchOpen === searchOpen) {
return;
}
navigate(
{ pathname, search: locationSearch },
{ replace: true, state: { ...locationState, wasSearchOpen: searchOpen } },
);
}, [searchOpen]);
useEffect(() => {
if (query === undefined && searchString !== undefined) {
setSearchString('');
setSearchOpen(false);
return;
}
if (searchString === '' && !!query) {
if (query && searchString !== query) {
setSearchString(query);
setSearchOpen(true);
updateSearchOpenState(true);
}
}, [query]);
@@ -117,7 +144,7 @@ export const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
return (
<Tooltip title={t('search.title.search')}>
<IconButton onClick={openSearch}>
<IconButton onClick={() => updateSearchOpenState(true)}>
<SearchIcon />
</IconButton>
</Tooltip>

View File

@@ -161,7 +161,7 @@ export const SearchAll: React.FC = () => {
const { setTitle, setAction } = useContext(NavBarContext);
useSetDefaultBackTo('sources/all/search');
useSetDefaultBackTo('sources');
const [query] = useQueryParam('query', StringParam);
const searchString = useDebounce(query, TRIGGER_SEARCH_THRESHOLD);

View File

@@ -205,19 +205,17 @@ export function SourceMangas() {
const { sourceId } = useParams<{ sourceId: string }>();
const navigate = useNavigate();
const { pathname, state: locationState } =
useLocation<{
contentType: SourceContentType;
filtersToApply: IPos[];
clearCache: boolean;
}>() ?? {};
const {
contentType: currentLocationContentType = SourceContentType.POPULAR,
filtersToApply: currentLocationFiltersToApply = [],
clearCache = false,
} = locationState ?? {};
} = useLocation<{
contentType: SourceContentType;
filtersToApply: IPos[];
clearCache: boolean;
}>().state ?? {};
useSetDefaultBackTo(pathname);
useSetDefaultBackTo('sources');
const { options } = useLibraryOptionsContext();
const [query] = useQueryParam('query', StringParam);