mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
Search box component (#1579)
This commit is contained in:
parent
8235f34921
commit
009bd39cb0
43
frontend/app/element/search.scss
Normal file
43
frontend/app/element/search.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
127
frontend/app/element/search.stories.tsx
Normal file
127
frontend/app/element/search.stories.tsx
Normal file
@ -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<typeof Search> = {
|
||||||
|
title: "Elements/Search",
|
||||||
|
component: Search,
|
||||||
|
args: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Popover>;
|
||||||
|
|
||||||
|
export const DefaultSearch: Story = {
|
||||||
|
render: (args) => {
|
||||||
|
const props = useSearch();
|
||||||
|
const setIsOpen = useSetAtom(props.isOpenAtom);
|
||||||
|
useEffect(() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="viewbox"
|
||||||
|
ref={props.anchorRef as React.RefObject<HTMLDivElement>}
|
||||||
|
style={{
|
||||||
|
border: "2px solid black",
|
||||||
|
width: "100%",
|
||||||
|
height: "200px",
|
||||||
|
background: "var(--main-bg-color)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Search {...args} {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className="viewbox"
|
||||||
|
ref={props.anchorRef as React.RefObject<HTMLDivElement>}
|
||||||
|
style={{
|
||||||
|
border: "2px solid black",
|
||||||
|
width: "100%",
|
||||||
|
height: "200px",
|
||||||
|
background: "var(--main-bg-color)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Search {...args} {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className="viewbox"
|
||||||
|
ref={props.anchorRef as React.RefObject<HTMLDivElement>}
|
||||||
|
style={{
|
||||||
|
border: "2px solid black",
|
||||||
|
width: "100%",
|
||||||
|
height: "200px",
|
||||||
|
background: "var(--main-bg-color)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Search {...args} {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className="viewbox"
|
||||||
|
ref={props.anchorRef as React.RefObject<HTMLDivElement>}
|
||||||
|
style={{
|
||||||
|
border: "2px solid black",
|
||||||
|
width: "100%",
|
||||||
|
height: "200px",
|
||||||
|
background: "var(--main-bg-color)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Search {...args} {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
args: {},
|
||||||
|
};
|
115
frontend/app/element/search.tsx
Normal file
115
frontend/app/element/search.tsx
Normal file
@ -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<string>;
|
||||||
|
indexAtom: PrimitiveAtom<number>;
|
||||||
|
numResultsAtom: PrimitiveAtom<number>;
|
||||||
|
isOpenAtom: PrimitiveAtom<boolean>;
|
||||||
|
anchorRef?: React.RefObject<HTMLElement>;
|
||||||
|
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 && (
|
||||||
|
<FloatingPortal>
|
||||||
|
<div className="search-container" style={{ ...floatingStyles }} {...dismiss} ref={refs.setFloating}>
|
||||||
|
<Input placeholder="Search" value={search} onChange={setSearch} />
|
||||||
|
<div
|
||||||
|
className={clsx("search-results", { hidden: numResults === 0 })}
|
||||||
|
aria-live="polite"
|
||||||
|
aria-label="Search Results"
|
||||||
|
>
|
||||||
|
{index + 1}/{numResults}
|
||||||
|
</div>
|
||||||
|
<div className="right-buttons">
|
||||||
|
<IconButton decl={prevDecl} />
|
||||||
|
<IconButton decl={nextDecl} />
|
||||||
|
<IconButton decl={closeDecl} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FloatingPortal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Search = memo(SearchComponent) as typeof SearchComponent;
|
||||||
|
|
||||||
|
export function useSearch(anchorRef?: React.RefObject<HTMLElement>): 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 };
|
||||||
|
}
|
@ -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<typeof SearchInput> = {
|
|
||||||
title: "Elements/SearchInput",
|
|
||||||
component: SearchInput,
|
|
||||||
argTypes: {
|
|
||||||
className: {
|
|
||||||
description: "Custom class for styling the input group",
|
|
||||||
control: { type: "text" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof SearchInput>;
|
|
||||||
|
|
||||||
export const DefaultSearchInput: Story = {
|
|
||||||
render: (args) => {
|
|
||||||
const handleSearch = () => {
|
|
||||||
console.log("Search triggered");
|
|
||||||
};
|
|
||||||
|
|
||||||
return <SearchInput />;
|
|
||||||
},
|
|
||||||
args: {
|
|
||||||
className: "custom-search-input",
|
|
||||||
},
|
|
||||||
};
|
|
@ -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 (
|
|
||||||
<InputGroup className="search-input-group">
|
|
||||||
<Input placeholder="Search..." />
|
|
||||||
<InputRightElement>
|
|
||||||
<Button className="search-button ghost grey">
|
|
||||||
<i className="fa-sharp fa-solid fa-magnifying-glass"></i>
|
|
||||||
</Button>
|
|
||||||
</InputRightElement>
|
|
||||||
</InputGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { SearchInput };
|
|
@ -83,8 +83,10 @@
|
|||||||
--modal-bg-color: #232323;
|
--modal-bg-color: #232323;
|
||||||
--modal-header-bottom-border-color: rgba(241, 246, 243, 0.15);
|
--modal-header-bottom-border-color: rgba(241, 246, 243, 0.15);
|
||||||
--modal-border-color: rgba(255, 255, 255, 0.12); /* toggle colors */
|
--modal-border-color: rgba(255, 255, 255, 0.12); /* toggle colors */
|
||||||
|
--modal-border-radius: 6px;
|
||||||
--toggle-bg-color: var(--border-color);
|
--toggle-bg-color: var(--border-color);
|
||||||
--modal-shadow-color: rgba(0, 0, 0, 0.8);
|
--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-thumb-color: var(--main-text-color);
|
||||||
--toggle-checked-bg-color: var(--accent-color);
|
--toggle-checked-bg-color: var(--accent-color);
|
||||||
|
Loading…
Reference in New Issue
Block a user