This commit is contained in:
Brian McGee
2025-07-31 12:05:24 +01:00
parent 045332ba5e
commit 283fa31649
2 changed files with 100 additions and 61 deletions

View File

@@ -4,7 +4,7 @@ import {
ComboboxRootProps,
} from "@kobalte/core/combobox";
import { FieldProps } from "./Field";
import { Accessor, createSignal, For, Show } from "solid-js";
import { Accessor, createEffect, createSignal, For, Show } from "solid-js";
import Icon from "../Icon/Icon";
import cx from "classnames";
import { Typography } from "@/src/components/Typography/Typography";
@@ -28,13 +28,15 @@ interface MachineTag {
export type MachineTagsProps = FieldProps & ComboboxRootProps<MachineTag> & {};
const deduplicateOptions = (options: MachineTag[]) => {
return options.filter((option, index) => {
return options.findIndex((o) => o.value === option.value) === index;
const uniqueOptions = (options: MachineTag[]) => {
const record: Record<string, MachineTag> = {};
options.forEach((option) => {
record[option.value] = option;
});
return Object.values(record);
};
const sortOptions = (options: MachineTag[]) => {
const sortedOptions = (options: MachineTag[]) => {
return options.sort((a, b) => {
if (a.disabled && !b.disabled) return -1;
return a.value.localeCompare(b.value);
@@ -56,63 +58,57 @@ const ItemComponent = (props: { item: CollectionNode<MachineTag> }) => {
);
};
const Control = (props: {
inverted?: boolean;
readOnly?: boolean;
disabled?: boolean;
}) => (
<Combobox.Control<MachineTag> class="control">
{(state) => (
<div class="selected-options">
<For each={state.selectedOptions()}>
{(option) => (
<Tag
label={option.value}
inverted={props.inverted}
action={
option.disabled || props.disabled || props.readOnly
? undefined
: {
icon: "Close",
onClick: () => state.remove(option),
}
}
/>
)}
</For>
<Show when={!props.readOnly}>
<div class="input-container">
<Combobox.Input />
<Combobox.Trigger class="trigger">
<Combobox.Icon class="icon">
<Icon icon="Expand" inverted={!props.inverted} size="100%" />
</Combobox.Icon>
</Combobox.Trigger>
</div>
</Show>
</div>
)}
</Combobox.Control>
);
export const MachineTags = (props: MachineTagsProps) => {
// options which are applied to all machines and cannot be interacted with
const alwaysOptions = [{ value: "All", disabled: true }];
const [options, setOptions] = createSignal<MachineTag[]>(
sortOptions([
const [selectedOptions, setSelectedOptions] = createSignal<MachineTag[]>([
...alwaysOptions,
...((props.defaultValue as MachineTag[]) || []),
]);
const [availableOptions, setAvailableOptions] = createSignal<MachineTag[]>(
sortedOptions([
...alwaysOptions,
...((props.defaultValue as MachineTag[]) || []),
]),
);
const _setOptions = (options: MachineTag[] | null) => {
if (options === null) setOptions([]);
setOptions(
sortOptions(deduplicateOptions([...alwaysOptions, ...(options || [])])),
const _setAvailableOptions = (options: MachineTag[] | null) => {
if (options === null) setAvailableOptions([...alwaysOptions]);
setAvailableOptions(
sortedOptions(uniqueOptions([...alwaysOptions, ...(options || [])])),
);
};
const _setSelectedOptions = (options: MachineTag[] | null) => {
if (options === null) setSelectedOptions([...alwaysOptions]);
setSelectedOptions(
sortedOptions(uniqueOptions([...alwaysOptions, ...(options || [])])),
);
};
const addSelectedOption = (option: MachineTag) => {
_setAvailableOptions([...availableOptions(), option]);
setSelectedOptions(
sortedOptions(uniqueOptions([...selectedOptions(), option])),
);
};
const removeSelectedOption = (option: MachineTag) => {
setSelectedOptions(
selectedOptions().filter((o) => o.value !== option.value),
)
}
createEffect(() => {
console.log("selectedOptions", selectedOptions());
console.log("availableOptions", availableOptions());
});
const align = () => {
if (props.readOnly) {
return "center";
@@ -121,6 +117,20 @@ export const MachineTags = (props: MachineTagsProps) => {
}
};
const onKeyDown = (event) => {
if (event.key === "Enter") {
event.preventDefault();
event.stopPropagation();
const input = event.currentTarget as HTMLInputElement;
if (input.value === "") return;
addSelectedOption({ value: input.value });
input.value = "";
}
};
return (
<Combobox<MachineTag>
multiple
@@ -129,14 +139,13 @@ export const MachineTags = (props: MachineTagsProps) => {
ghost: props.ghost,
})}
{...props}
itemComponent={ItemComponent}
defaultValue={options()}
value={options()}
value={selectedOptions()}
options={availableOptions()}
optionLabel={(option) => option.value}
optionTextValue="value"
options={options()}
onChange={_setOptions}
placeholder="Tag a machine"
itemComponent={ItemComponent}
onChange={_setSelectedOptions}
placeholder="Enter a tag name"
>
<Orienter orientation={props.orientation} align={align()}>
<Label
@@ -147,11 +156,40 @@ export const MachineTags = (props: MachineTagsProps) => {
{/*<KCombobox.HiddenSelect {...props.input} />*/}
<Control
inverted={props.inverted}
readOnly={props.readOnly}
disabled={props.disabled}
/>
<Combobox.Control<MachineTag> class="control">
<div class="selected-options">
<For each={selectedOptions()}>
{(option) => (
<Tag
label={option.value}
inverted={props.inverted}
action={
option.disabled || props.disabled || props.readOnly
? undefined
: {
icon: "Close",
onClick: () => removeSelectedOption(option),
}
}
/>
)}
</For>
<Show when={!props.readOnly}>
<div class="input-container">
<Combobox.Input onKeyDown={onKeyDown} />
<Combobox.Trigger class="trigger">
<Combobox.Icon class="icon">
<Icon
icon="Expand"
inverted={!props.inverted}
size="100%"
/>
</Combobox.Icon>
</Combobox.Trigger>
</div>
</Show>
</div>
</Combobox.Control>
</Orienter>
<Combobox.Portal>

View File

@@ -146,6 +146,7 @@ export const Default: Story = {
readOnly={!editing}
orientation="horizontal"
multiple
defaultValue={[{ value: "Foo"}, {value: "bar"}, {value: "baz"}]}
/>
)}
</Field>