Select: add simple select dropdown for single select

This commit is contained in:
Johannes Kirschbauer
2025-08-05 13:35:01 +02:00
parent 90ef55f040
commit 60ba00dd8f
3 changed files with 277 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
.trigger {
@apply bg-def-1 flex flex-grow justify-between items-center gap-2 h-7 px-2 py-1;
@apply rounded-[4px];
&[data-expanded] {
@apply outline-def-2 outline-1 outline;
z-index: 40;
}
&[data-highlighted] {
@apply outline outline-1 outline-inv-2
}
&:hover {
@apply bg-def-2;
& .icon {
@apply bg-def-1 text-fg-def-1;
}
}
&:active {
@apply bg-def-4;
& .icon {
@apply bg-inv-4 text-fg-inv-1;
}
}
&:focus-visible {
@apply outline-def-2 outline-1 outline;
}
}
.icon {
@apply bg-def-2 rounded-sm;
@apply flex items-center justify-center h-[14px] w-[14px] p-[2px];
&[data-disabled] {
@apply cursor-not-allowed;
}
}
.options_content {
@apply bg-def-1 px-1 py-3 rounded-[4px] -mt-8 pt-10 -mx-1;
@apply outline-def-2 outline-1 outline;
transform-origin: var(--kb-popper-content-transform-origin);
&[data-expanded] {
animation: overlayShow 250ms ease-out;
}
/* Option elements (typically <li>) */
& [role="option"] {
@apply px-1 py-2 rounded-sm flex items-center gap-1 flex-shrink-0 ;
&[data-highlighted],
&:focus-visible {
@apply outline outline-1 outline-inv-2
}
&:hover {
@apply bg-def-2;
}
&:active {
@apply bg-def-4;
}
}
& [role="listbox"] {
&:focus-visible {
@apply outline-none;
}
}
}
@keyframes overlayShow {
from {
opacity: 0;
transform: scaleY(0.94);
}
to {
opacity: 1;
transform: scaleY(1);
}
}

View File

@@ -0,0 +1,71 @@
import { TagProps } from "@/src/components/Tag/Tag";
import { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
import { Select, SelectProps } from "./Select";
import { Fieldset } from "../Form/Fieldset";
// const meta: Meta<SelectProps> = {
// title: "Components/Select",
// component: Select,
// };
const meta = {
title: "Components/Form/Select",
component: Select,
decorators: [
(Story: StoryObj, context: StoryContext<SelectProps>) => {
return (
<div class={`w-[600px]`}>
<Fieldset>
<Story />
</Fieldset>
</div>
);
},
],
} satisfies Meta<SelectProps>;
export default meta;
type Story = StoryObj<TagProps>;
export const Default: Story = {
args: {
required: true,
label: {
label: "Select your pet",
description: "Choose your favorite pet from the list",
},
options: [
{ value: "dog", label: "Doggy" },
{ value: "cat", label: "Catty" },
{ value: "fish", label: "Fishy" },
{ value: "bird", label: "Birdy" },
{ value: "hamster", label: "Hammy" },
{ value: "snake", label: "Snakey" },
{ value: "turtle", label: "Turtly" },
],
placeholder: "Select your pet",
},
};
// <Field name="language">
// {(field, input) => (
// <Select
// required
// label={{
// label: "Language",
// description: "Select your preferred language",
// }}
// options={[
// { value: "en", label: "English" },
// { value: "fr", label: "Français" },
// ]}
// placeholder="Language"
// onChange={(opt) => {
// setValue(formStore, "language", opt?.value || "");
// }}
// name={field.name}
// validationState={field.error ? "invalid" : "valid"}
// />
// )}
// </Field>

View File

@@ -0,0 +1,118 @@
import { Select as KSelect } from "@kobalte/core/select";
import Icon from "../Icon/Icon";
import { Orienter } from "../Form/Orienter";
import { Label, LabelProps } from "../Form/Label";
import { createEffect, createSignal, JSX, splitProps } from "solid-js";
import styles from "./Select.module.css";
import { Typography } from "../Typography/Typography";
import cx from "classnames";
export interface Option { value: string; label: string; disabled?: boolean }
export interface SelectProps {
// Kobalte Select props, for modular forms
name: string;
placeholder?: string | undefined;
options: Option[];
value: string | undefined;
error: string;
required?: boolean | undefined;
disabled?: boolean | undefined;
ref: (element: HTMLSelectElement) => void;
onInput: JSX.EventHandler<HTMLSelectElement, InputEvent>;
onChange: JSX.EventHandler<HTMLSelectElement, Event>;
onBlur: JSX.EventHandler<HTMLSelectElement, FocusEvent>;
// Custom props
orientation?: "horizontal" | "vertical";
label?: Omit<LabelProps, "labelComponent" | "descriptionComponent">;
}
export const Select = (props: SelectProps) => {
const [root, selectProps] = splitProps(
props,
["name", "placeholder", "options", "required", "disabled"],
["placeholder", "ref", "onInput", "onChange", "onBlur"],
);
const [getValue, setValue] = createSignal<Option>();
createEffect(() => {
setValue(props.options.find((option) => props.value === option.value));
});
return (
<KSelect
{...root}
sameWidth={true}
gutter={0}
multiple={false}
value={getValue()}
onChange={setValue}
optionValue="value"
optionTextValue="label"
optionDisabled="disabled"
validationState={props.error ? "invalid" : "valid"}
itemComponent={(props) => (
<KSelect.Item item={props.item} class="flex gap-1 p-2">
<KSelect.ItemIndicator>
<Icon icon="Checkmark" />
</KSelect.ItemIndicator>
<KSelect.ItemLabel>
<Typography
hierarchy="body"
size="xs"
weight="bold"
class="flex w-full items-center"
>
{props.item.rawValue.label}
</Typography>
</KSelect.ItemLabel>
</KSelect.Item>
)}
placeholder={
<Typography
hierarchy="body"
size="xs"
weight="bold"
class="flex w-full items-center"
>
{props.placeholder}
</Typography>
}
>
<Orienter orientation={props.orientation || "horizontal"}>
<Label
{...props.label}
labelComponent={KSelect.Label}
descriptionComponent={KSelect.Description}
validationState={props.error ? "invalid" : "valid"}
/>
<KSelect.HiddenSelect {...selectProps} />
<KSelect.Trigger class={cx(styles.trigger)}>
<KSelect.Value<Option>>
{(state) => (
<Typography
hierarchy="body"
size="xs"
weight="bold"
class="flex w-full items-center"
>
{state.selectedOption().label}
</Typography>
)}
</KSelect.Value>
<KSelect.Icon as="button" class={styles.icon}>
<Icon icon="Expand" color="inherit" />
</KSelect.Icon>
</KSelect.Trigger>
</Orienter>
<KSelect.Portal>
<KSelect.Content class={styles.options_content}>
<KSelect.Listbox />
</KSelect.Content>
</KSelect.Portal>
{/* TODO: Display error next to the problem */}
{/* <KSelect.ErrorMessage>{props.error}</KSelect.ErrorMessage> */}
</KSelect>
);
};