feat: add search item cursor

This commit is contained in:
Sonny
2023-05-17 12:55:59 +02:00
parent e19bffb792
commit 261254576b
6 changed files with 79 additions and 26 deletions

View File

@@ -33,14 +33,14 @@ export default function Modal({
onClick={handleWrapperClick} onClick={handleWrapperClick}
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { duration: 0.1 } }} exit={{ opacity: 0, transition: { duration: 0.1, delay: 0.1 } }}
> >
<motion.div <motion.div
className={styles["modal-container"]} className={styles["modal-container"]}
style={{ padding }} style={{ padding }}
initial={{ opacity: 0, y: -15 }} initial={{ opacity: 0, y: "-6em" }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -15, transition: { duration: 0.1 } }} exit={{ opacity: 0, y: "-6em", transition: { duration: 0.1 } }}
> >
{!noHeader && ( {!noHeader && (
<div className={styles["modal-header"]}> <div className={styles["modal-header"]}>

View File

@@ -1,4 +1,4 @@
import { ReactNode } from "react"; import { ReactNode, useMemo } from "react";
import { SearchItem } from "types"; import { SearchItem } from "types";
import SearchListItem from "./SearchListItem"; import SearchListItem from "./SearchListItem";
@@ -8,14 +8,18 @@ import styles from "./search.module.scss";
export default function SearchList({ export default function SearchList({
items, items,
noItem, noItem,
cursor,
setCursor,
}: { }: {
items: SearchItem[]; items: SearchItem[];
noItem?: ReactNode; noItem?: ReactNode;
cursor: number;
setCursor: (cursor: number) => void;
}) { }) {
return ( return (
<ul className={styles["search-list"]}> <ul className={styles["search-list"]}>
{items.length > 0 ? ( {items.length > 0 ? (
items.map((item) => ( items.map((item, index) => (
<SearchListItem <SearchListItem
item={{ item={{
id: item.id, id: item.id,
@@ -23,6 +27,9 @@ export default function SearchList({
url: item.url, url: item.url,
type: item.type, type: item.type,
}} }}
setCursor={setCursor}
selected={index === cursor}
index={index}
key={item.type + "-" + item.id} key={item.type + "-" + item.id}
/> />
)) ))

View File

@@ -1,23 +1,66 @@
import { motion } from "framer-motion";
import LinkTag from "next/link"; import LinkTag from "next/link";
import { AiOutlineFolder } from "react-icons/ai"; import { AiOutlineFolder } from "react-icons/ai";
import LinkFavicon from "components/Links/LinkFavicon"; import LinkFavicon from "components/Links/LinkFavicon";
import { SearchItem } from "types"; import { SearchItem } from "types";
import { useEffect, useId, useRef, useState } from "react";
import styles from "./search.module.scss"; import styles from "./search.module.scss";
export default function SearchListItem({ item }: { item: SearchItem }) { export default function SearchListItem({
item,
index,
selected,
setCursor,
}: {
item: SearchItem;
index?: number;
selected: boolean;
setCursor: (cursor: number) => void;
}) {
const id = useId();
const ref = useRef<HTMLLIElement>(null);
const [isHover, setHover] = useState<boolean>(false);
const { name, type, url } = item; const { name, type, url } = item;
useEffect(() => {
if (selected && !isHover) {
console.log(selected, ref.current);
ref.current?.scrollIntoView({ behavior: "smooth", block: "center" });
}
}, [isHover, selected]);
return ( return (
<li className={styles["search-item"]}> <motion.li
className={styles["search-item"]}
initial={{ y: -15, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{
type: "spring",
stiffness: 260,
damping: 20,
delay: index * 0.025,
}}
ref={ref}
onMouseEnter={() => {
setCursor(index);
setHover(true);
}}
onMouseLeave={() => setHover(false)}
key={id}
>
<LinkTag href={url} target="_blank" rel="no-referrer"> <LinkTag href={url} target="_blank" rel="no-referrer">
{type === "link" ? ( {type === "link" ? (
<LinkFavicon url={item.url} noMargin /> <LinkFavicon url={item.url} noMargin size={24} />
) : ( ) : (
<AiOutlineFolder size={32} /> <AiOutlineFolder size={24} />
)} )}
<span>{name}</span> <span>{name}</span>
{selected && "selected"}
</LinkTag> </LinkTag>
</li> </motion.li>
); );
} }

View File

@@ -11,7 +11,7 @@ import SearchList from "./SearchList";
import * as Keys from "constants/keys"; import * as Keys from "constants/keys";
import { GOOGLE_SEARCH_URL } from "constants/search-urls"; import { GOOGLE_SEARCH_URL } from "constants/search-urls";
import { Category, Link, SearchItem } from "types"; import { Category, SearchItem } from "types";
import styles from "./search.module.scss"; import styles from "./search.module.scss";
@@ -19,13 +19,11 @@ export default function SearchModal({
close, close,
handleSelectCategory, handleSelectCategory,
categories, categories,
favorites,
items, items,
}: { }: {
close: any; close: any;
handleSelectCategory: (category: Category) => void; handleSelectCategory: (category: Category) => void;
categories: Category[]; categories: Category[];
favorites: Link[];
items: SearchItem[]; items: SearchItem[];
}) { }) {
const autoFocusRef = useAutoFocus(); const autoFocusRef = useAutoFocus();
@@ -51,12 +49,16 @@ export default function SearchModal({
[items, search] [items, search]
); );
useHotkeys(Keys.ARROW_LEFT, () => { useHotkeys(Keys.ARROW_UP, () => setCursor((cursor) => (cursor -= 1)), {
console.log("left"); enableOnFormTags: ["INPUT"],
enabled: itemsCompletion.length > 1 && cursor !== 0,
preventDefault: true,
}); });
useHotkeys(Keys.ARROW_DOWN, () => setCursor((cursor) => (cursor += 1)), {
useHotkeys(Keys.ARROW_RIGHT, () => { enableOnFormTags: ["INPUT"],
console.log("right"); enabled:
itemsCompletion.length > 1 && cursor !== itemsCompletion.length - 1,
preventDefault: true,
}); });
const handleSearchInputChange = useCallback((value) => { const handleSearchInputChange = useCallback((value) => {
@@ -74,19 +76,17 @@ export default function SearchModal({
return close(); return close();
} }
// TODO: replace "firstItem" by a "cursor" const selectedItem = itemsCompletion[cursor];
const firstItem = itemsCompletion[0]; const category = categories.find((c) => c.id === selectedItem.id);
if (selectedItem.type === "category" && category) {
const category = categories.find((c) => c.id === firstItem.id);
if (firstItem.type === "category" && category) {
handleSelectCategory(category); handleSelectCategory(category);
return close(); return close();
} }
window.open(firstItem.url); window.open(selectedItem.url);
close(); close();
}, },
[categories, close, handleSelectCategory, itemsCompletion, search] [categories, close, cursor, handleSelectCategory, itemsCompletion, search]
); );
return ( return (
@@ -121,6 +121,8 @@ export default function SearchModal({
type: item.type, type: item.type,
}))} }))}
noItem={<LabelSearchWithGoogle />} noItem={<LabelSearchWithGoogle />}
cursor={cursor}
setCursor={setCursor}
/> />
)} )}
<button type="submit" disabled={!canSubmit} style={{ display: "none" }}> <button type="submit" disabled={!canSubmit} style={{ display: "none" }}>

View File

@@ -48,6 +48,7 @@ function Home(props: HomeProps) {
name: item.name, name: item.name,
url: type === "link" ? (item as Link).url : `/?categoryId=${item.id}`, url: type === "link" ? (item as Link).url : `/?categoryId=${item.id}`,
type, type,
category: type === "link" ? (item as Link).category : undefined,
}); });
const itemsSearch = useMemo<SearchItem[]>(() => { const itemsSearch = useMemo<SearchItem[]>(() => {
@@ -179,7 +180,6 @@ function Home(props: HomeProps) {
<SearchModal <SearchModal
close={modal.close} close={modal.close}
categories={categories} categories={categories}
favorites={favorites}
items={itemsSearch} items={itemsSearch}
handleSelectCategory={handleSelectCategory} handleSelectCategory={handleSelectCategory}
/> />

1
src/types.d.ts vendored
View File

@@ -32,4 +32,5 @@ export interface SearchItem {
name: string; name: string;
url: string; url: string;
type: "category" | "link"; type: "category" | "link";
category?: undefined | Link["category"];
} }