From 009bd39cb0e45903bf28ecaf3158ff73731a1d3d Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Thu, 19 Dec 2024 13:49:48 -0800 Subject: [PATCH] Search box component (#1579) --- frontend/app/element/search.scss | 43 +++++++ frontend/app/element/search.stories.tsx | 127 +++++++++++++++++++ frontend/app/element/search.tsx | 115 +++++++++++++++++ frontend/app/element/searchinput.scss | 0 frontend/app/element/searchinput.stories.tsx | 32 ----- frontend/app/element/searchinput.tsx | 20 --- frontend/app/theme.scss | 2 + 7 files changed, 287 insertions(+), 52 deletions(-) create mode 100644 frontend/app/element/search.scss create mode 100644 frontend/app/element/search.stories.tsx create mode 100644 frontend/app/element/search.tsx delete mode 100644 frontend/app/element/searchinput.scss delete mode 100644 frontend/app/element/searchinput.stories.tsx delete mode 100644 frontend/app/element/searchinput.tsx diff --git a/frontend/app/element/search.scss b/frontend/app/element/search.scss new file mode 100644 index 000000000..6a1f68f9b --- /dev/null +++ b/frontend/app/element/search.scss @@ -0,0 +1,43 @@ +.search-container { + display: flex; + flex-direction: row; + background-color: var(--modal-bg-color); + border: 1px solid var(--accent-color); + border-radius: var(--modal-border-radius); + box-shadow: var(--modal-box-shadow); + color: var(--main-text-color); + padding: 5px 5px 5px 10px; + gap: 5px; + width: 50%; + max-width: 300px; + min-width: 200px; + + input { + flex: 1 1 auto; + border: none; + font-size: 14px; + height: 100%; + padding: 0; + border-radius: 0; + } + + .search-results { + font-size: 12px; + margin: auto 0; + color: var(--secondary-text-color); + + &.hidden { + display: none; + } + } + + .right-buttons { + display: flex; + gap: 5px; + padding-left: 5px; + border-left: 1px solid var(--modal-border-color); + button { + font-size: 12px; + } + } +} diff --git a/frontend/app/element/search.stories.tsx b/frontend/app/element/search.stories.tsx new file mode 100644 index 000000000..c44fd54d6 --- /dev/null +++ b/frontend/app/element/search.stories.tsx @@ -0,0 +1,127 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import type { Meta, StoryObj } from "@storybook/react"; +import { useSetAtom } from "jotai"; +import { useEffect } from "react"; +import { Popover } from "./popover"; +import { Search, useSearch } from "./search"; + +const meta: Meta = { + title: "Elements/Search", + component: Search, + args: {}, +}; + +export default meta; +type Story = StoryObj; + +export const DefaultSearch: Story = { + render: (args) => { + const props = useSearch(); + const setIsOpen = useSetAtom(props.isOpenAtom); + useEffect(() => { + setIsOpen(true); + }, []); + return ( +
} + style={{ + border: "2px solid black", + width: "100%", + height: "200px", + background: "var(--main-bg-color)", + }} + > + +
+ ); + }, + args: {}, +}; + +export const Results10: Story = { + render: (args) => { + const props = useSearch(); + const setIsOpen = useSetAtom(props.isOpenAtom); + const setNumResults = useSetAtom(props.numResultsAtom); + useEffect(() => { + setIsOpen(true); + setNumResults(10); + }, []); + return ( +
} + style={{ + border: "2px solid black", + width: "100%", + height: "200px", + background: "var(--main-bg-color)", + }} + > + +
+ ); + }, + args: {}, +}; + +export const InputAndResults10: Story = { + render: (args) => { + const props = useSearch(); + const setIsOpen = useSetAtom(props.isOpenAtom); + const setNumResults = useSetAtom(props.numResultsAtom); + const setSearch = useSetAtom(props.searchAtom); + useEffect(() => { + setIsOpen(true); + setNumResults(10); + setSearch("search term"); + }, []); + return ( +
} + style={{ + border: "2px solid black", + width: "100%", + height: "200px", + background: "var(--main-bg-color)", + }} + > + +
+ ); + }, + args: {}, +}; + +export const LongInputAndResults10: Story = { + render: (args) => { + const props = useSearch(); + const setIsOpen = useSetAtom(props.isOpenAtom); + const setNumResults = useSetAtom(props.numResultsAtom); + const setSearch = useSetAtom(props.searchAtom); + useEffect(() => { + setIsOpen(true); + setNumResults(10); + setSearch("search term ".repeat(10).trimEnd()); + }, []); + return ( +
} + style={{ + border: "2px solid black", + width: "100%", + height: "200px", + background: "var(--main-bg-color)", + }} + > + +
+ ); + }, + args: {}, +}; diff --git a/frontend/app/element/search.tsx b/frontend/app/element/search.tsx new file mode 100644 index 000000000..8a18e753c --- /dev/null +++ b/frontend/app/element/search.tsx @@ -0,0 +1,115 @@ +import { autoUpdate, FloatingPortal, Middleware, offset, useDismiss, useFloating } from "@floating-ui/react"; +import clsx from "clsx"; +import { atom, PrimitiveAtom, useAtom, useAtomValue } from "jotai"; +import { memo, useCallback, useRef, useState } from "react"; +import { IconButton } from "./iconbutton"; +import { Input } from "./input"; +import "./search.scss"; + +type SearchProps = { + searchAtom: PrimitiveAtom; + indexAtom: PrimitiveAtom; + numResultsAtom: PrimitiveAtom; + isOpenAtom: PrimitiveAtom; + anchorRef?: React.RefObject; + offsetX?: number; + offsetY?: number; +}; + +const SearchComponent = ({ + searchAtom, + indexAtom, + numResultsAtom, + isOpenAtom, + anchorRef, + offsetX = 10, + offsetY = 10, +}: SearchProps) => { + const [isOpen, setIsOpen] = useAtom(isOpenAtom); + const [search, setSearch] = useAtom(searchAtom); + const [index, setIndex] = useAtom(indexAtom); + const numResults = useAtomValue(numResultsAtom); + + const handleOpenChange = useCallback((open: boolean) => { + setIsOpen(open); + }, []); + + const middleware: Middleware[] = []; + middleware.push( + offset(({ rects }) => ({ + mainAxis: -rects.floating.height - offsetY, + crossAxis: -offsetX, + })) + ); + + const { refs, floatingStyles, context } = useFloating({ + placement: "top-end", + open: isOpen, + onOpenChange: handleOpenChange, + whileElementsMounted: autoUpdate, + middleware, + elements: { + reference: anchorRef!.current, + }, + }); + + const dismiss = useDismiss(context); + + const prevDecl: IconButtonDecl = { + elemtype: "iconbutton", + icon: "chevron-up", + title: "Previous Result", + disabled: index === 0, + click: () => setIndex(index - 1), + }; + + const nextDecl: IconButtonDecl = { + elemtype: "iconbutton", + icon: "chevron-down", + title: "Next Result", + disabled: !numResults || index === numResults - 1, + click: () => setIndex(index + 1), + }; + + const closeDecl: IconButtonDecl = { + elemtype: "iconbutton", + icon: "xmark-large", + title: "Close", + click: () => setIsOpen(false), + }; + + return ( + <> + {isOpen && ( + +
+ +
+ {index + 1}/{numResults} +
+
+ + + +
+
+
+ )} + + ); +}; + +export const Search = memo(SearchComponent) as typeof SearchComponent; + +export function useSearch(anchorRef?: React.RefObject): SearchProps { + const [searchAtom] = useState(atom("")); + const [indexAtom] = useState(atom(0)); + const [numResultsAtom] = useState(atom(0)); + const [isOpenAtom] = useState(atom(false)); + anchorRef ??= useRef(null); + return { searchAtom, indexAtom, numResultsAtom, isOpenAtom, anchorRef }; +} diff --git a/frontend/app/element/searchinput.scss b/frontend/app/element/searchinput.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/app/element/searchinput.stories.tsx b/frontend/app/element/searchinput.stories.tsx deleted file mode 100644 index 359b0d8e3..000000000 --- a/frontend/app/element/searchinput.stories.tsx +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2024, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import type { Meta, StoryObj } from "@storybook/react"; -import { SearchInput } from "./searchinput"; - -const meta: Meta = { - title: "Elements/SearchInput", - component: SearchInput, - argTypes: { - className: { - description: "Custom class for styling the input group", - control: { type: "text" }, - }, - }, -}; - -export default meta; -type Story = StoryObj; - -export const DefaultSearchInput: Story = { - render: (args) => { - const handleSearch = () => { - console.log("Search triggered"); - }; - - return ; - }, - args: { - className: "custom-search-input", - }, -}; diff --git a/frontend/app/element/searchinput.tsx b/frontend/app/element/searchinput.tsx deleted file mode 100644 index b977a5197..000000000 --- a/frontend/app/element/searchinput.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2024, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { Button } from "./button"; -import { Input, InputGroup, InputRightElement } from "./input"; - -const SearchInput = () => { - return ( - - - - - - - ); -}; - -export { SearchInput }; diff --git a/frontend/app/theme.scss b/frontend/app/theme.scss index 9f25430c4..ac5d1cf85 100644 --- a/frontend/app/theme.scss +++ b/frontend/app/theme.scss @@ -83,8 +83,10 @@ --modal-bg-color: #232323; --modal-header-bottom-border-color: rgba(241, 246, 243, 0.15); --modal-border-color: rgba(255, 255, 255, 0.12); /* toggle colors */ + --modal-border-radius: 6px; --toggle-bg-color: var(--border-color); --modal-shadow-color: rgba(0, 0, 0, 0.8); + --modal-box-shadow: box-shadow: 0px 8px 24px 0px var(--modal-shadow-color); --toggle-thumb-color: var(--main-text-color); --toggle-checked-bg-color: var(--accent-color);