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-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);
|
||||
|
Loading…
Reference in New Issue
Block a user