ui: add multiple search for machines and tags
This commit is contained in:
201
pkgs/clan-app/ui/src/components/Search/MultipleSearch.tsx
Normal file
201
pkgs/clan-app/ui/src/components/Search/MultipleSearch.tsx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import Icon from "../Icon/Icon";
|
||||||
|
import { Button } from "../Button/Button";
|
||||||
|
import styles from "./Search.module.css";
|
||||||
|
import { Combobox } from "@kobalte/core/combobox";
|
||||||
|
import { createMemo, createSignal, For, JSX } from "solid-js";
|
||||||
|
import { createVirtualizer, VirtualizerOptions } from "@tanstack/solid-virtual";
|
||||||
|
import { CollectionNode } from "@kobalte/core/*";
|
||||||
|
|
||||||
|
export interface Option {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemRenderOptions {
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchMultipleProps<T> {
|
||||||
|
onChange: (values: T[]) => void;
|
||||||
|
options: T[];
|
||||||
|
renderItem: (item: T, opts: ItemRenderOptions) => JSX.Element;
|
||||||
|
initialValues?: T[];
|
||||||
|
placeholder?: string;
|
||||||
|
virtualizerOptions?: Partial<VirtualizerOptions<Element, Element>>;
|
||||||
|
height: string; // e.g. '14.5rem'
|
||||||
|
}
|
||||||
|
export function SearchMultiple<T extends Option>(
|
||||||
|
props: SearchMultipleProps<T>,
|
||||||
|
) {
|
||||||
|
// Controlled input value, to allow resetting the input itself
|
||||||
|
const [values, setValues] = createSignal<T[]>(props.initialValues || []);
|
||||||
|
const [inputValue, setInputValue] = createSignal<string>("");
|
||||||
|
|
||||||
|
let inputEl: HTMLInputElement;
|
||||||
|
|
||||||
|
let listboxRef: HTMLUListElement;
|
||||||
|
|
||||||
|
// const [currentItems, setItems] = createSignal(ALL_MODULES_OPTIONS_FLAT);
|
||||||
|
const [comboboxItems, setComboboxItems] = createSignal<CollectionNode<T>[]>(
|
||||||
|
props.options.map((item) => ({
|
||||||
|
rawValue: item,
|
||||||
|
})) as CollectionNode<T>[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a reactive virtualizer that updates when items change
|
||||||
|
const virtualizer = createMemo(() => {
|
||||||
|
const items = comboboxItems();
|
||||||
|
|
||||||
|
const newVirtualizer = createVirtualizer({
|
||||||
|
count: items.length,
|
||||||
|
getScrollElement: () => listboxRef,
|
||||||
|
getItemKey: (index) => {
|
||||||
|
const item = items[index];
|
||||||
|
return item?.rawValue?.value || `item-${index}`;
|
||||||
|
},
|
||||||
|
estimateSize: () => 42,
|
||||||
|
gap: 6,
|
||||||
|
overscan: 5,
|
||||||
|
...props.virtualizerOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return newVirtualizer;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Combobox<T>
|
||||||
|
multiple
|
||||||
|
value={values()}
|
||||||
|
onChange={(values) => {
|
||||||
|
setValues(() => values);
|
||||||
|
// setInputValue(value ? value.label : "");
|
||||||
|
props.onChange(values);
|
||||||
|
}}
|
||||||
|
class={styles.searchContainer}
|
||||||
|
style={{ "--container-height": props.height }}
|
||||||
|
placement="bottom-start"
|
||||||
|
options={props.options}
|
||||||
|
optionValue="value"
|
||||||
|
optionTextValue="label"
|
||||||
|
optionLabel="label"
|
||||||
|
sameWidth={true}
|
||||||
|
open={true}
|
||||||
|
gutter={7}
|
||||||
|
modal={false}
|
||||||
|
flip={false}
|
||||||
|
virtualized={true}
|
||||||
|
allowsEmptyCollection={true}
|
||||||
|
closeOnSelection={false}
|
||||||
|
triggerMode="manual"
|
||||||
|
noResetInputOnBlur={true}
|
||||||
|
>
|
||||||
|
<Combobox.Control<T> class={styles.searchHeader}>
|
||||||
|
{(state) => (
|
||||||
|
<div class={styles.inputContainer}>
|
||||||
|
<Icon icon="Search" color="quaternary" />
|
||||||
|
<Combobox.Input
|
||||||
|
ref={(el) => {
|
||||||
|
inputEl = el;
|
||||||
|
}}
|
||||||
|
class={styles.searchInput}
|
||||||
|
placeholder={props.placeholder}
|
||||||
|
value={inputValue()}
|
||||||
|
onChange={(e) => {
|
||||||
|
setInputValue(e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="reset"
|
||||||
|
hierarchy="primary"
|
||||||
|
size="s"
|
||||||
|
ghost
|
||||||
|
icon="CloseCircle"
|
||||||
|
onClick={() => {
|
||||||
|
state.clear();
|
||||||
|
setInputValue("");
|
||||||
|
|
||||||
|
// Dispatch an input event to notify combobox listeners
|
||||||
|
inputEl.dispatchEvent(
|
||||||
|
new Event("input", { bubbles: true, cancelable: true }),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Combobox.Control>
|
||||||
|
<Combobox.Portal>
|
||||||
|
<Combobox.Content
|
||||||
|
class={styles.searchContent}
|
||||||
|
tabIndex={-1}
|
||||||
|
style={{ "--container-height": props.height }}
|
||||||
|
>
|
||||||
|
<Combobox.Listbox<T>
|
||||||
|
ref={(el) => {
|
||||||
|
listboxRef = el;
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
overflow: "auto",
|
||||||
|
"overflow-y": "auto",
|
||||||
|
}}
|
||||||
|
scrollToItem={(key) => {
|
||||||
|
const idx = comboboxItems().findIndex(
|
||||||
|
(option) => option.rawValue.value === key,
|
||||||
|
);
|
||||||
|
virtualizer().scrollToIndex(idx);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(items) => {
|
||||||
|
// Update the virtualizer with the filtered items
|
||||||
|
const arr = Array.from(items());
|
||||||
|
setComboboxItems(arr);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: `${virtualizer().getTotalSize()}px`,
|
||||||
|
width: "100%",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<For each={virtualizer().getVirtualItems()}>
|
||||||
|
{(virtualRow) => {
|
||||||
|
const item: CollectionNode<T> | undefined =
|
||||||
|
items().getItem(virtualRow.key as string);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
console.warn("Item not found for key:", virtualRow.key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const isSelected = () =>
|
||||||
|
values().some((v) => v.value === item.rawValue.value);
|
||||||
|
return (
|
||||||
|
<Combobox.Item
|
||||||
|
item={item}
|
||||||
|
class={styles.searchItem}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: `${virtualRow.size}px`,
|
||||||
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.renderItem(item.rawValue, {
|
||||||
|
selected: isSelected(),
|
||||||
|
})}
|
||||||
|
</Combobox.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Combobox.Listbox>
|
||||||
|
</Combobox.Content>
|
||||||
|
</Combobox.Portal>
|
||||||
|
</Combobox>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -42,17 +42,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.searchItem {
|
.searchItem {
|
||||||
@apply flex py-1 px-2 pr-4 gap-2 justify-between items-center rounded-md;
|
|
||||||
|
|
||||||
& [role="option"] {
|
|
||||||
@apply flex flex-col w-full;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Icon */
|
|
||||||
& [role="complementary"] {
|
|
||||||
@apply size-8 flex items-center justify-center bg-white rounded-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-highlighted],
|
&[data-highlighted],
|
||||||
&:focus,
|
&:focus,
|
||||||
&:focus-visible,
|
&:focus-visible,
|
||||||
@@ -63,12 +52,16 @@
|
|||||||
&:active {
|
&:active {
|
||||||
@apply bg-inv-acc-3;
|
@apply bg-inv-acc-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@apply flex flex-col justify-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchContainer {
|
.searchContainer {
|
||||||
@apply bg-gradient-to-b from-bg-inv-3 to-bg-inv-4;
|
@apply bg-gradient-to-b from-bg-inv-3 to-bg-inv-4;
|
||||||
|
|
||||||
@apply h-[14.5rem] rounded-lg;
|
@apply rounded-lg;
|
||||||
|
|
||||||
|
height: var(--container-height, 14.5rem);
|
||||||
|
|
||||||
border: 1px solid #2b4647;
|
border: 1px solid #2b4647;
|
||||||
|
|
||||||
@@ -87,7 +80,8 @@
|
|||||||
|
|
||||||
.searchContent {
|
.searchContent {
|
||||||
@apply px-3;
|
@apply px-3;
|
||||||
height: calc(14.5rem - 4rem);
|
height: var(--container-height, 14.5rem);
|
||||||
|
padding-bottom: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes contentHide {
|
@keyframes contentHide {
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||||
|
|
||||||
import { Search, SearchProps, Module } from "./Search";
|
import { Search, SearchProps } from "./Search";
|
||||||
|
import Icon from "../Icon/Icon";
|
||||||
|
import { Combobox } from "@kobalte/core/combobox";
|
||||||
|
import { Typography } from "../Typography/Typography";
|
||||||
|
import {
|
||||||
|
ItemRenderOptions,
|
||||||
|
SearchMultiple,
|
||||||
|
SearchMultipleProps,
|
||||||
|
} from "./MultipleSearch";
|
||||||
|
import { JSX, Show } from "solid-js";
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: "Components/Search",
|
title: "Components/Search",
|
||||||
component: Search,
|
component: Search,
|
||||||
} satisfies Meta<SearchProps>;
|
} satisfies Meta<SearchProps<unknown>>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
||||||
type Story = StoryObj<SearchProps>;
|
type Story = StoryObj<SearchProps<unknown>>;
|
||||||
|
|
||||||
// To test the virtualizer, we can generate a list of modules
|
// To test the virtualizer, we can generate a list of modules
|
||||||
function generateModules(count: number): Module[] {
|
function generateModules(count: number): Module[] {
|
||||||
@@ -45,7 +54,7 @@ function generateModules(count: number): Module[] {
|
|||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
modules.push({
|
modules.push({
|
||||||
value: `lolcat/module-${i + 1}`,
|
value: `lolcat/module-${i + 1}`,
|
||||||
name: `Module ${i + 1}`,
|
label: `Module ${i + 1}`,
|
||||||
description: `${greek[i % greek.length]}#${i + 1}`,
|
description: `${greek[i % greek.length]}#${i + 1}`,
|
||||||
input: "lolcat",
|
input: "lolcat",
|
||||||
});
|
});
|
||||||
@@ -54,15 +63,49 @@ function generateModules(count: number): Module[] {
|
|||||||
return modules;
|
return modules;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Module {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
input: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {
|
args: {
|
||||||
// Test with lots of modules
|
// Test with lots of modules
|
||||||
options: generateModules(1000),
|
options: generateModules(1000),
|
||||||
|
renderItem: (item: Module) => {
|
||||||
|
return (
|
||||||
|
<div class="flex items-center justify-between gap-2 rounded-md px-2 py-1 pr-4">
|
||||||
|
<div class="flex size-8 items-center justify-center rounded-md bg-white">
|
||||||
|
<Icon icon="Code" />
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full flex-col">
|
||||||
|
<Combobox.ItemLabel class="flex">
|
||||||
|
<Typography hierarchy="body" size="s" weight="medium" inverted>
|
||||||
|
{item.label}
|
||||||
|
</Typography>
|
||||||
|
</Combobox.ItemLabel>
|
||||||
|
<Typography
|
||||||
|
hierarchy="body"
|
||||||
|
size="xxs"
|
||||||
|
weight="normal"
|
||||||
|
color="quaternary"
|
||||||
|
inverted
|
||||||
|
class="flex justify-between"
|
||||||
|
>
|
||||||
|
<span>{item.description}</span>
|
||||||
|
<span>by {item.input}</span>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
render: (args: SearchProps) => {
|
render: (args: SearchProps<Module>) => {
|
||||||
return (
|
return (
|
||||||
<div class="absolute bottom-1/3 w-3/4 px-3">
|
<div class="absolute bottom-1/3 w-3/4 px-3">
|
||||||
<Search
|
<Search<Module>
|
||||||
{...args}
|
{...args}
|
||||||
onChange={(module) => {
|
onChange={(module) => {
|
||||||
// Go to the module configuration
|
// Go to the module configuration
|
||||||
@@ -73,3 +116,116 @@ export const Default: Story = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MachineOrTag =
|
||||||
|
| {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
type: "machine";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
members: string[];
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
type: "tag";
|
||||||
|
};
|
||||||
|
|
||||||
|
interface WrapIfProps {
|
||||||
|
condition: boolean;
|
||||||
|
wrapper: (children: JSX.Element) => JSX.Element;
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
const WrapIf = (props: WrapIfProps) => {
|
||||||
|
if (props.condition) {
|
||||||
|
return props.wrapper(props.children);
|
||||||
|
} else {
|
||||||
|
return props.children;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const machinesAndTags: MachineOrTag[] = [
|
||||||
|
{ value: "machine-1", label: "Machine 1", type: "machine" },
|
||||||
|
{ value: "machine-2", label: "Machine 2", type: "machine" },
|
||||||
|
{
|
||||||
|
value: "all",
|
||||||
|
label: "All",
|
||||||
|
type: "tag",
|
||||||
|
members: ["machine-1", "machine-2", "machine-3", "machine-4", "machine-5"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "tag-1",
|
||||||
|
label: "Tag 1",
|
||||||
|
type: "tag",
|
||||||
|
members: ["machine-1", "machine-2"],
|
||||||
|
},
|
||||||
|
{ value: "machine-3", label: "Machine 3", type: "machine" },
|
||||||
|
{ value: "machine-4", label: "Machine 4", type: "machine" },
|
||||||
|
{ value: "machine-5", label: "Machine 5", type: "machine" },
|
||||||
|
{
|
||||||
|
value: "tag-2",
|
||||||
|
label: "Tag 2",
|
||||||
|
type: "tag",
|
||||||
|
members: ["machine-3", "machine-4", "machine-5"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const Multiple: Story = {
|
||||||
|
args: {
|
||||||
|
// Test with lots of modules
|
||||||
|
options: machinesAndTags,
|
||||||
|
placeholder: "Search for Machine or Tags",
|
||||||
|
renderItem: (item: MachineOrTag, opts: ItemRenderOptions) => {
|
||||||
|
console.log("Rendering item:", item, "opts", opts);
|
||||||
|
return (
|
||||||
|
<div class="flex w-full items-center gap-2 px-3 py-2">
|
||||||
|
<Combobox.ItemIndicator>
|
||||||
|
<Show when={opts.selected} fallback={<Icon icon="Code" />}>
|
||||||
|
<Icon icon="Checkmark" color="primary" inverted />
|
||||||
|
</Show>
|
||||||
|
</Combobox.ItemIndicator>
|
||||||
|
<Combobox.ItemLabel class="flex items-center gap-2">
|
||||||
|
<Typography hierarchy="body" size="s" weight="medium" inverted>
|
||||||
|
{item.label}
|
||||||
|
</Typography>
|
||||||
|
<Show when={item.type === "tag" && item}>
|
||||||
|
{(tag) => (
|
||||||
|
<Typography
|
||||||
|
hierarchy="body"
|
||||||
|
size="xs"
|
||||||
|
weight="medium"
|
||||||
|
inverted
|
||||||
|
color="secondary"
|
||||||
|
tag="div"
|
||||||
|
>
|
||||||
|
{tag().members.length}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</Combobox.ItemLabel>
|
||||||
|
<Icon
|
||||||
|
class="ml-auto"
|
||||||
|
icon={item.type === "machine" ? "Machine" : "Tag"}
|
||||||
|
color="quaternary"
|
||||||
|
inverted
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
render: (args: SearchMultipleProps<MachineOrTag>) => {
|
||||||
|
return (
|
||||||
|
<div class="absolute bottom-1/3 w-3/4 px-3">
|
||||||
|
<SearchMultiple<MachineOrTag>
|
||||||
|
{...args}
|
||||||
|
height="20rem"
|
||||||
|
virtualizerOptions={{
|
||||||
|
estimateSize: () => 38,
|
||||||
|
}}
|
||||||
|
onChange={(selection) => {
|
||||||
|
// Go to the module configuration
|
||||||
|
console.log("Currently Selected:", selection);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,25 +2,23 @@ import Icon from "../Icon/Icon";
|
|||||||
import { Button } from "../Button/Button";
|
import { Button } from "../Button/Button";
|
||||||
import styles from "./Search.module.css";
|
import styles from "./Search.module.css";
|
||||||
import { Combobox } from "@kobalte/core/combobox";
|
import { Combobox } from "@kobalte/core/combobox";
|
||||||
import { createMemo, createSignal, For } from "solid-js";
|
import { createMemo, createSignal, For, JSX } from "solid-js";
|
||||||
import { Typography } from "../Typography/Typography";
|
|
||||||
import { createVirtualizer } from "@tanstack/solid-virtual";
|
import { createVirtualizer } from "@tanstack/solid-virtual";
|
||||||
import { CollectionNode } from "@kobalte/core/*";
|
import { CollectionNode } from "@kobalte/core/*";
|
||||||
|
|
||||||
export interface Module {
|
export interface Option {
|
||||||
value: string;
|
value: string;
|
||||||
name: string;
|
label: string;
|
||||||
input: string;
|
|
||||||
description: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchProps {
|
export interface SearchProps<T> {
|
||||||
onChange: (value: Module | null) => void;
|
onChange: (value: T | null) => void;
|
||||||
options: Module[];
|
options: T[];
|
||||||
|
renderItem: (item: T) => JSX.Element;
|
||||||
}
|
}
|
||||||
export function Search(props: SearchProps) {
|
export function Search<T extends Option>(props: SearchProps<T>) {
|
||||||
// Controlled input value, to allow resetting the input itself
|
// Controlled input value, to allow resetting the input itself
|
||||||
const [value, setValue] = createSignal<Module | null>(null);
|
const [value, setValue] = createSignal<T | null>(null);
|
||||||
const [inputValue, setInputValue] = createSignal<string>("");
|
const [inputValue, setInputValue] = createSignal<string>("");
|
||||||
|
|
||||||
let inputEl: HTMLInputElement;
|
let inputEl: HTMLInputElement;
|
||||||
@@ -28,12 +26,10 @@ export function Search(props: SearchProps) {
|
|||||||
let listboxRef: HTMLUListElement;
|
let listboxRef: HTMLUListElement;
|
||||||
|
|
||||||
// const [currentItems, setItems] = createSignal(ALL_MODULES_OPTIONS_FLAT);
|
// const [currentItems, setItems] = createSignal(ALL_MODULES_OPTIONS_FLAT);
|
||||||
const [comboboxItems, setComboboxItems] = createSignal<
|
const [comboboxItems, setComboboxItems] = createSignal<CollectionNode<T>[]>(
|
||||||
CollectionNode<Module>[]
|
|
||||||
>(
|
|
||||||
props.options.map((item) => ({
|
props.options.map((item) => ({
|
||||||
rawValue: item,
|
rawValue: item,
|
||||||
})) as CollectionNode<Module>[],
|
})) as CollectionNode<T>[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a reactive virtualizer that updates when items change
|
// Create a reactive virtualizer that updates when items change
|
||||||
@@ -56,19 +52,19 @@ export function Search(props: SearchProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox<Module>
|
<Combobox<T>
|
||||||
value={value()}
|
value={value()}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setValue(value);
|
setValue(() => value);
|
||||||
setInputValue(value ? value.name : "");
|
setInputValue(value ? value.label : "");
|
||||||
props.onChange(value);
|
props.onChange(value);
|
||||||
}}
|
}}
|
||||||
class={styles.searchContainer}
|
class={styles.searchContainer}
|
||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
options={props.options}
|
options={props.options}
|
||||||
optionValue="value"
|
optionValue="value"
|
||||||
optionTextValue="name"
|
optionTextValue="label"
|
||||||
optionLabel="name"
|
optionLabel="label"
|
||||||
placeholder="Search a service"
|
placeholder="Search a service"
|
||||||
sameWidth={true}
|
sameWidth={true}
|
||||||
open={true}
|
open={true}
|
||||||
@@ -81,7 +77,7 @@ export function Search(props: SearchProps) {
|
|||||||
triggerMode="manual"
|
triggerMode="manual"
|
||||||
noResetInputOnBlur={true}
|
noResetInputOnBlur={true}
|
||||||
>
|
>
|
||||||
<Combobox.Control<Module> class={styles.searchHeader}>
|
<Combobox.Control<T> class={styles.searchHeader}>
|
||||||
{(state) => (
|
{(state) => (
|
||||||
<div class={styles.inputContainer}>
|
<div class={styles.inputContainer}>
|
||||||
<Icon icon="Search" color="quaternary" />
|
<Icon icon="Search" color="quaternary" />
|
||||||
@@ -117,7 +113,7 @@ export function Search(props: SearchProps) {
|
|||||||
</Combobox.Control>
|
</Combobox.Control>
|
||||||
<Combobox.Portal>
|
<Combobox.Portal>
|
||||||
<Combobox.Content class={styles.searchContent} tabIndex={-1}>
|
<Combobox.Content class={styles.searchContent} tabIndex={-1}>
|
||||||
<Combobox.Listbox<Module>
|
<Combobox.Listbox<T>
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
listboxRef = el;
|
listboxRef = el;
|
||||||
}}
|
}}
|
||||||
@@ -149,7 +145,7 @@ export function Search(props: SearchProps) {
|
|||||||
>
|
>
|
||||||
<For each={virtualizer().getVirtualItems()}>
|
<For each={virtualizer().getVirtualItems()}>
|
||||||
{(virtualRow) => {
|
{(virtualRow) => {
|
||||||
const item: CollectionNode<Module> | undefined =
|
const item: CollectionNode<T> | undefined =
|
||||||
items().getItem(virtualRow.key as string);
|
items().getItem(virtualRow.key as string);
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
@@ -169,32 +165,7 @@ export function Search(props: SearchProps) {
|
|||||||
transform: `translateY(${virtualRow.start}px)`,
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div role="complementary">
|
{props.renderItem(item.rawValue)}
|
||||||
<Icon icon="Code" />
|
|
||||||
</div>
|
|
||||||
<div role="option">
|
|
||||||
<Combobox.ItemLabel class="flex">
|
|
||||||
<Typography
|
|
||||||
hierarchy="body"
|
|
||||||
size="s"
|
|
||||||
weight="medium"
|
|
||||||
inverted
|
|
||||||
>
|
|
||||||
{item.rawValue.name}
|
|
||||||
</Typography>
|
|
||||||
</Combobox.ItemLabel>
|
|
||||||
<Typography
|
|
||||||
hierarchy="body"
|
|
||||||
size="xxs"
|
|
||||||
weight="normal"
|
|
||||||
color="quaternary"
|
|
||||||
inverted
|
|
||||||
class="flex justify-between"
|
|
||||||
>
|
|
||||||
<span>{item.rawValue.description}</span>
|
|
||||||
<span>by {item.rawValue.input}</span>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</Combobox.Item>
|
</Combobox.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user