Select: add simple select dropdown for single select
This commit is contained in:
88
pkgs/clan-app/ui/src/components/Select/Select.module.css
Normal file
88
pkgs/clan-app/ui/src/components/Select/Select.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
71
pkgs/clan-app/ui/src/components/Select/Select.stories.tsx
Normal file
71
pkgs/clan-app/ui/src/components/Select/Select.stories.tsx
Normal 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>
|
||||
118
pkgs/clan-app/ui/src/components/Select/Select.tsx
Normal file
118
pkgs/clan-app/ui/src/components/Select/Select.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user