ui/form: use css modules for form components
This commit is contained in:
@@ -13,6 +13,7 @@ import styles from "./Checkbox.module.css";
|
|||||||
import { FieldProps } from "./Field";
|
import { FieldProps } from "./Field";
|
||||||
import { Orienter } from "./Orienter";
|
import { Orienter } from "./Orienter";
|
||||||
import { Match, mergeProps, splitProps, Switch } from "solid-js";
|
import { Match, mergeProps, splitProps, Switch } from "solid-js";
|
||||||
|
import { keepTruthy } from "@/src/util";
|
||||||
|
|
||||||
export type CheckboxProps = FieldProps &
|
export type CheckboxProps = FieldProps &
|
||||||
KCheckboxRootProps & {
|
KCheckboxRootProps & {
|
||||||
@@ -62,6 +63,9 @@ export const Checkbox = (props: CheckboxProps) => {
|
|||||||
<Label
|
<Label
|
||||||
labelComponent={KCheckbox.Label}
|
labelComponent={KCheckbox.Label}
|
||||||
descriptionComponent={KCheckbox.Description}
|
descriptionComponent={KCheckbox.Description}
|
||||||
|
in={keepTruthy(
|
||||||
|
local.orientation == "horizontal" && "Orienter-horizontal",
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<KCheckbox.Input {...local.input} />
|
<KCheckbox.Input {...local.input} />
|
||||||
|
|||||||
@@ -5,17 +5,17 @@
|
|||||||
@apply mb-2.5 w-full;
|
@apply mb-2.5 w-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.fields {
|
.fields {
|
||||||
@apply flex flex-col gap-4 w-full rounded-md;
|
@apply flex flex-col gap-4 w-full rounded-md;
|
||||||
@apply px-4 py-5 bg-def-2;
|
@apply px-4 py-5 bg-def-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.error {
|
.error {
|
||||||
@apply w-full;
|
@apply w-full py-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.inverted {
|
&.inverted {
|
||||||
div.fields {
|
.fields {
|
||||||
@apply bg-inv-2;
|
@apply bg-inv-2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import "./Fieldset.css";
|
import styles from "./Fieldset.module.css";
|
||||||
import { JSX, splitProps } from "solid-js";
|
import { JSX } from "solid-js";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { Typography } from "@/src/components/Typography/Typography";
|
import { Typography } from "@/src/components/Typography/Typography";
|
||||||
import { FieldProps } from "./Field";
|
import { FieldProps } from "./Field";
|
||||||
@@ -21,23 +21,15 @@ export type FieldsetProps = Pick<FieldProps, "orientation" | "inverted"> & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Fieldset = (props: FieldsetProps) => {
|
export const Fieldset = (props: FieldsetProps) => {
|
||||||
const [fieldProps] = splitProps(props, [
|
|
||||||
"orientation",
|
|
||||||
"inverted",
|
|
||||||
"disabled",
|
|
||||||
"error",
|
|
||||||
"children",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const children = () =>
|
const children = () =>
|
||||||
typeof props.children === "function"
|
typeof props.children === "function"
|
||||||
? props.children(fieldProps)
|
? props.children(props)
|
||||||
: props.children;
|
: props.children;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
class={cx("fieldset", { inverted: props.inverted })}
|
class={cx(styles.fieldset, { [styles.inverted]: props.inverted })}
|
||||||
aria-disabled={props.disabled || undefined}
|
aria-disabled={props.disabled || undefined}
|
||||||
>
|
>
|
||||||
{props.legend && (
|
{props.legend && (
|
||||||
@@ -55,9 +47,9 @@ export const Fieldset = (props: FieldsetProps) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</legend>
|
</legend>
|
||||||
)}
|
)}
|
||||||
<div class="fields">{children()}</div>
|
<div class={styles.fields}>{children()}</div>
|
||||||
{props.error && (
|
{props.error && (
|
||||||
<div class="error" role="alert">
|
<div class={styles.error} role="alert">
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="body"
|
hierarchy="body"
|
||||||
size="xxs"
|
size="xxs"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Orienter } from "./Orienter";
|
|||||||
import { createSignal, splitProps } from "solid-js";
|
import { createSignal, splitProps } from "solid-js";
|
||||||
import { Tooltip } from "@kobalte/core/tooltip";
|
import { Tooltip } from "@kobalte/core/tooltip";
|
||||||
import { Typography } from "@/src/components/Typography/Typography";
|
import { Typography } from "@/src/components/Typography/Typography";
|
||||||
|
import { keepTruthy } from "@/src/util";
|
||||||
|
|
||||||
export type HostFileInputProps = FieldProps &
|
export type HostFileInputProps = FieldProps &
|
||||||
TextFieldRootProps & {
|
TextFieldRootProps & {
|
||||||
@@ -40,8 +41,7 @@ export const HostFileInput = (props: HostFileInputProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [styleProps, otherProps] = splitProps(props, [
|
const [local, other] = splitProps(props, [
|
||||||
"class",
|
|
||||||
"size",
|
"size",
|
||||||
"orientation",
|
"orientation",
|
||||||
"inverted",
|
"inverted",
|
||||||
@@ -49,51 +49,40 @@ export const HostFileInput = (props: HostFileInputProps) => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField {...other}>
|
||||||
class={cx(
|
|
||||||
styleProps.class,
|
|
||||||
"form-field",
|
|
||||||
styleProps.size,
|
|
||||||
styleProps.orientation,
|
|
||||||
{
|
|
||||||
inverted: styleProps.inverted,
|
|
||||||
ghost: styleProps.ghost,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<Orienter
|
<Orienter
|
||||||
orientation={styleProps.orientation}
|
orientation={local.orientation}
|
||||||
align={styleProps.orientation == "horizontal" ? "center" : "start"}
|
align={local.orientation == "horizontal" ? "center" : "start"}
|
||||||
>
|
>
|
||||||
<Label
|
<Label
|
||||||
labelComponent={TextField.Label}
|
labelComponent={TextField.Label}
|
||||||
descriptionComponent={TextField.Description}
|
descriptionComponent={TextField.Description}
|
||||||
|
in={keepTruthy(
|
||||||
|
local.orientation == "horizontal" && "Orienter-horizontal",
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
{...props.input}
|
|
||||||
hidden={true}
|
hidden={true}
|
||||||
value={value()}
|
value={value()}
|
||||||
ref={(el: HTMLInputElement) => {
|
ref={(el: HTMLInputElement) => {
|
||||||
actualInputElement = el; // Capture for local use
|
actualInputElement = el; // Capture for local use
|
||||||
}}
|
}}
|
||||||
|
{...props.input}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!value() && (
|
{!value() && (
|
||||||
<Button
|
<Button
|
||||||
hierarchy="secondary"
|
hierarchy="secondary"
|
||||||
size={styleProps.size}
|
size={local.size}
|
||||||
icon="Folder"
|
icon="Folder"
|
||||||
onClick={selectFile}
|
onClick={selectFile}
|
||||||
disabled={props.disabled || props.readOnly}
|
disabled={other.disabled || other.readOnly}
|
||||||
elasticity={
|
elasticity={local.orientation === "vertical" ? "fit" : undefined}
|
||||||
styleProps.orientation === "vertical" ? "fit" : undefined
|
|
||||||
}
|
|
||||||
in={
|
in={
|
||||||
styleProps.orientation == "horizontal"
|
local.orientation == "horizontal"
|
||||||
? `HostFileInput-${styleProps.orientation}`
|
? `HostFileInput-${local.orientation}`
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -104,12 +93,16 @@ export const HostFileInput = (props: HostFileInputProps) => {
|
|||||||
{value() && (
|
{value() && (
|
||||||
<Tooltip placement="top">
|
<Tooltip placement="top">
|
||||||
<Tooltip.Portal>
|
<Tooltip.Portal>
|
||||||
<Tooltip.Content class={styles.tooltipContent}>
|
<Tooltip.Content
|
||||||
|
class={cx(styles.tooltipContent, {
|
||||||
|
[styles.inverted]: local.inverted,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="body"
|
hierarchy="body"
|
||||||
size="xs"
|
size="xs"
|
||||||
weight="medium"
|
weight="medium"
|
||||||
inverted={!styleProps.inverted}
|
inverted={!local.inverted}
|
||||||
>
|
>
|
||||||
{value()}
|
{value()}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -118,16 +111,14 @@ export const HostFileInput = (props: HostFileInputProps) => {
|
|||||||
</Tooltip.Portal>
|
</Tooltip.Portal>
|
||||||
<Tooltip.Trigger
|
<Tooltip.Trigger
|
||||||
as={Button}
|
as={Button}
|
||||||
elasticity={
|
elasticity={local.orientation === "vertical" ? "fit" : undefined}
|
||||||
styleProps.orientation === "vertical" ? "fit" : undefined
|
|
||||||
}
|
|
||||||
in={
|
in={
|
||||||
styleProps.orientation == "horizontal"
|
local.orientation == "horizontal"
|
||||||
? `HostFileInput-${styleProps.orientation}`
|
? `HostFileInput-${local.orientation}`
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
hierarchy="secondary"
|
hierarchy="secondary"
|
||||||
size={styleProps.size}
|
size={local.size}
|
||||||
icon="Folder"
|
icon="Folder"
|
||||||
onClick={selectFile}
|
onClick={selectFile}
|
||||||
disabled={props.disabled || props.readOnly}
|
disabled={props.disabled || props.readOnly}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
div.form-label {
|
.label {
|
||||||
@apply flex flex-col gap-1 w-full;
|
@apply flex flex-col gap-1 w-full;
|
||||||
|
|
||||||
& > span,
|
& > span,
|
||||||
@@ -14,3 +14,6 @@ div.form-label {
|
|||||||
@apply flex items-center gap-1;
|
@apply flex items-center gap-1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.label.in-Orienter-horizontal {
|
||||||
|
@apply w-1/2 shrink;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Show } from "solid-js";
|
import { mergeProps, Show } from "solid-js";
|
||||||
import { Typography } from "@/src/components/Typography/Typography";
|
import { Typography } from "@/src/components/Typography/Typography";
|
||||||
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
|
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
|
||||||
import Icon from "@/src/components/Icon/Icon";
|
import Icon from "@/src/components/Icon/Icon";
|
||||||
@@ -6,7 +6,9 @@ import { TextField } from "@kobalte/core/text-field";
|
|||||||
import { Checkbox } from "@kobalte/core/checkbox";
|
import { Checkbox } from "@kobalte/core/checkbox";
|
||||||
import { Combobox } from "@kobalte/core/combobox";
|
import { Combobox } from "@kobalte/core/combobox";
|
||||||
import { Select } from "@kobalte/core/select";
|
import { Select } from "@kobalte/core/select";
|
||||||
import "./Label.css";
|
import styles from "./Label.module.css";
|
||||||
|
import cx from "classnames";
|
||||||
|
import { getInClasses } from "@/src/util";
|
||||||
|
|
||||||
export type Size = "default" | "s";
|
export type Size = "default" | "s";
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ export type DescriptionComponent =
|
|||||||
| typeof Combobox.Description
|
| typeof Combobox.Description
|
||||||
| typeof Select.Description;
|
| typeof Select.Description;
|
||||||
|
|
||||||
|
type In = "Orienter-horizontal";
|
||||||
export interface LabelProps {
|
export interface LabelProps {
|
||||||
labelComponent: LabelComponent;
|
labelComponent: LabelComponent;
|
||||||
descriptionComponent: DescriptionComponent;
|
descriptionComponent: DescriptionComponent;
|
||||||
@@ -34,52 +37,57 @@ export interface LabelProps {
|
|||||||
inverted?: boolean;
|
inverted?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
validationState?: "valid" | "invalid";
|
validationState?: "valid" | "invalid";
|
||||||
|
in?: In | In[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Label = (props: LabelProps) => {
|
export const Label = (props: LabelProps) => {
|
||||||
|
const local = mergeProps(
|
||||||
|
{ size: "default", labelWeight: "bold", validationState: "valid" } as const,
|
||||||
|
props,
|
||||||
|
);
|
||||||
const descriptionSize = () => (props.size == "default" ? "s" : "xs");
|
const descriptionSize = () => (props.size == "default" ? "s" : "xs");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={props.label}>
|
<Show when={local.label}>
|
||||||
<div class="form-label">
|
<div class={cx(styles.label, getInClasses(styles, local.in))}>
|
||||||
<props.labelComponent>
|
<local.labelComponent>
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="label"
|
hierarchy="label"
|
||||||
size={props.size || "default"}
|
size={local.size}
|
||||||
color={props.validationState == "invalid" ? "error" : "primary"}
|
color={local.validationState == "invalid" ? "error" : "primary"}
|
||||||
weight={props.labelWeight || "bold"}
|
weight={local.labelWeight}
|
||||||
inverted={props.inverted}
|
inverted={local.inverted}
|
||||||
in="Label"
|
in="Label"
|
||||||
>
|
>
|
||||||
{props.label}
|
{local.label}
|
||||||
</Typography>
|
</Typography>
|
||||||
{props.tooltip && (
|
{local.tooltip && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
placement="top"
|
placement="top"
|
||||||
inverted={props.inverted}
|
inverted={local.inverted}
|
||||||
description={props.tooltip}
|
description={local.tooltip}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon="Info"
|
icon="Info"
|
||||||
color="tertiary"
|
color="tertiary"
|
||||||
inverted={props.inverted}
|
inverted={local.inverted}
|
||||||
size={props.size == "default" ? "0.85em" : "0.75rem"}
|
size={local.size == "default" ? "0.85em" : "0.75rem"}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</props.labelComponent>
|
</local.labelComponent>
|
||||||
{props.description && (
|
{local.description && (
|
||||||
<props.descriptionComponent>
|
<local.descriptionComponent>
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="body"
|
hierarchy="body"
|
||||||
size={descriptionSize()}
|
size={descriptionSize()}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
weight="normal"
|
weight="normal"
|
||||||
inverted={props.inverted}
|
inverted={local.inverted}
|
||||||
>
|
>
|
||||||
{props.description}
|
{local.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
</props.descriptionComponent>
|
</local.descriptionComponent>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export const MachineTags = (props: MachineTagsProps) => {
|
|||||||
return (
|
return (
|
||||||
<Combobox<MachineTag>
|
<Combobox<MachineTag>
|
||||||
multiple
|
multiple
|
||||||
class={cx("form-field", styles.machineTags, props.orientation)}
|
class={cx(styles.machineTags, props.orientation)}
|
||||||
{...splitProps(props, ["defaultValue"])[1]}
|
{...splitProps(props, ["defaultValue"])[1]}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
value={selectedOptions()}
|
value={selectedOptions()}
|
||||||
@@ -196,6 +196,9 @@ export const MachineTags = (props: MachineTagsProps) => {
|
|||||||
<Label
|
<Label
|
||||||
labelComponent={Combobox.Label}
|
labelComponent={Combobox.Label}
|
||||||
descriptionComponent={Combobox.Description}
|
descriptionComponent={Combobox.Description}
|
||||||
|
in={keepTruthy(
|
||||||
|
props.orientation == "horizontal" && "Orienter-horizontal",
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
div.orienter {
|
.orienter {
|
||||||
@apply flex flex-col gap-2 w-full;
|
@apply flex flex-col gap-2 w-full;
|
||||||
|
|
||||||
&.vertical {
|
&.vertical {
|
||||||
@@ -6,10 +6,6 @@ div.orienter {
|
|||||||
|
|
||||||
&.horizontal {
|
&.horizontal {
|
||||||
@apply flex-row justify-between gap-0;
|
@apply flex-row justify-between gap-0;
|
||||||
|
|
||||||
& > div.form-label {
|
|
||||||
@apply w-1/2 shrink;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.align-center {
|
&.align-center {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { JSX } from "solid-js";
|
import { JSX, mergeProps } from "solid-js";
|
||||||
|
|
||||||
import "./Orienter.css";
|
import styles from "./Orienter.module.css";
|
||||||
|
|
||||||
export interface OrienterProps {
|
export interface OrienterProps {
|
||||||
orientation?: "vertical" | "horizontal";
|
orientation?: "vertical" | "horizontal";
|
||||||
@@ -10,11 +10,17 @@ export interface OrienterProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Orienter = (props: OrienterProps) => {
|
export const Orienter = (props: OrienterProps) => {
|
||||||
const alignment = () => `align-${props.align || "center"}`;
|
const local = mergeProps({ align: "center" } as const, props);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={cx("orienter", alignment(), props.orientation)}>
|
<div
|
||||||
{props.children}
|
class={cx(
|
||||||
|
styles.orienter,
|
||||||
|
styles[`align-${local.align}`],
|
||||||
|
local.orientation && styles[local.orientation],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{local.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { createEffect, createSignal, splitProps } from "solid-js";
|
|||||||
import "./TextInput.css";
|
import "./TextInput.css";
|
||||||
import { FieldProps } from "./Field";
|
import { FieldProps } from "./Field";
|
||||||
import { Orienter } from "./Orienter";
|
import { Orienter } from "./Orienter";
|
||||||
|
import { keepTruthy } from "@/src/util";
|
||||||
|
|
||||||
export type TextAreaProps = FieldProps &
|
export type TextAreaProps = FieldProps &
|
||||||
TextFieldRootProps & {
|
TextFieldRootProps & {
|
||||||
@@ -117,6 +118,9 @@ export const TextArea = (props: TextAreaProps) => {
|
|||||||
<Label
|
<Label
|
||||||
labelComponent={TextField.Label}
|
labelComponent={TextField.Label}
|
||||||
descriptionComponent={TextField.Description}
|
descriptionComponent={TextField.Description}
|
||||||
|
in={keepTruthy(
|
||||||
|
props.orientation == "horizontal" && "Orienter-horizontal",
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<TextField.TextArea
|
<TextField.TextArea
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
onMount,
|
onMount,
|
||||||
splitProps,
|
splitProps,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
|
import { keepTruthy } from "@/src/util";
|
||||||
|
|
||||||
export type TextInputProps = FieldProps &
|
export type TextInputProps = FieldProps &
|
||||||
TextFieldRootProps & {
|
TextFieldRootProps & {
|
||||||
@@ -88,6 +89,9 @@ export const TextInput = (props: TextInputProps) => {
|
|||||||
<Label
|
<Label
|
||||||
labelComponent={TextField.Label}
|
labelComponent={TextField.Label}
|
||||||
descriptionComponent={TextField.Description}
|
descriptionComponent={TextField.Description}
|
||||||
|
in={keepTruthy(
|
||||||
|
props.orientation == "horizontal" && "Orienter-horizontal",
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.options_content {
|
.optionsContent {
|
||||||
z-index: var(--z-index);
|
z-index: var(--z-index);
|
||||||
|
|
||||||
@apply bg-def-1 px-1 py-3 rounded-[4px] -mt-8 pt-10 -mx-1;
|
@apply bg-def-1 px-1 py-3 rounded-[4px] -mt-8 pt-10 -mx-1;
|
||||||
|
|||||||
@@ -2,11 +2,19 @@ import { Select as KSelect, SelectPortalProps } from "@kobalte/core/select";
|
|||||||
import Icon from "../Icon/Icon";
|
import Icon from "../Icon/Icon";
|
||||||
import { Orienter } from "../Form/Orienter";
|
import { Orienter } from "../Form/Orienter";
|
||||||
import { Label, LabelProps } from "../Form/Label";
|
import { Label, LabelProps } from "../Form/Label";
|
||||||
import { createEffect, createSignal, JSX, Show, splitProps } from "solid-js";
|
import {
|
||||||
|
createEffect,
|
||||||
|
createSignal,
|
||||||
|
JSX,
|
||||||
|
mergeProps,
|
||||||
|
Show,
|
||||||
|
splitProps,
|
||||||
|
} from "solid-js";
|
||||||
import styles from "./Select.module.css";
|
import styles from "./Select.module.css";
|
||||||
import { Typography } from "../Typography/Typography";
|
import { Typography } from "../Typography/Typography";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { useModalContext } from "../Modal/Modal";
|
import { useModalContext } from "../Modal/Modal";
|
||||||
|
import { keepTruthy } from "@/src/util";
|
||||||
|
|
||||||
export interface Option {
|
export interface Option {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -46,13 +54,19 @@ export type SelectProps = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const Select = (props: SelectProps) => {
|
export const Select = (props: SelectProps) => {
|
||||||
const [root, selectProps] = splitProps(
|
const [root, selectProps, rest] = splitProps(
|
||||||
props,
|
mergeProps(
|
||||||
|
{
|
||||||
|
orientation: "horizontal",
|
||||||
|
noOptionsText: "No options available",
|
||||||
|
} as const,
|
||||||
|
props,
|
||||||
|
),
|
||||||
["name", "placeholder", "required", "disabled"],
|
["name", "placeholder", "required", "disabled"],
|
||||||
["placeholder", "ref", "onInput", "onChange", "onBlur"],
|
["placeholder", "ref", "onInput", "onChange", "onBlur"],
|
||||||
);
|
);
|
||||||
|
|
||||||
const zIndex = () => props.zIndex ?? 40;
|
const zIndex = () => rest.zIndex ?? 40;
|
||||||
|
|
||||||
const [getValue, setValue] = createSignal<Option>();
|
const [getValue, setValue] = createSignal<Option>();
|
||||||
|
|
||||||
@@ -61,29 +75,29 @@ export const Select = (props: SelectProps) => {
|
|||||||
// Internal loading state for async options
|
// Internal loading state for async options
|
||||||
const [loading, setLoading] = createSignal(false);
|
const [loading, setLoading] = createSignal(false);
|
||||||
createEffect(async () => {
|
createEffect(async () => {
|
||||||
if (props.getOptions) {
|
if (rest.getOptions) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const options = await props.getOptions();
|
const options = await rest.getOptions();
|
||||||
setResolvedOptions(options);
|
setResolvedOptions(options);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
} else if (props.options) {
|
} else if (rest.options) {
|
||||||
setResolvedOptions(props.options);
|
setResolvedOptions(rest.options);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = () => props.options ?? resolvedOptions();
|
const options = () => rest.options ?? resolvedOptions();
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
console.log("options,", options());
|
console.log("options,", options());
|
||||||
setValue(options().find((option) => props.value === option.value));
|
setValue(options().find((option) => rest.value === option.value));
|
||||||
});
|
});
|
||||||
|
|
||||||
const modalContext = useModalContext();
|
const modalContext = useModalContext();
|
||||||
const defaultMount =
|
const defaultMount =
|
||||||
props.portalProps?.mount || modalContext?.portalRef || document.body;
|
rest.portalProps?.mount || modalContext?.portalRef || document.body;
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
console.debug("Select component mounted at:", defaultMount);
|
console.debug("Select component mounted at:", defaultMount);
|
||||||
@@ -101,7 +115,7 @@ export const Select = (props: SelectProps) => {
|
|||||||
optionValue="value"
|
optionValue="value"
|
||||||
optionTextValue="label"
|
optionTextValue="label"
|
||||||
optionDisabled="disabled"
|
optionDisabled="disabled"
|
||||||
validationState={props.error ? "invalid" : "valid"}
|
validationState={rest.error ? "invalid" : "valid"}
|
||||||
itemComponent={(props) => (
|
itemComponent={(props) => (
|
||||||
<KSelect.Item item={props.item} class="flex gap-1 p-2">
|
<KSelect.Item item={props.item} class="flex gap-1 p-2">
|
||||||
<KSelect.ItemIndicator>
|
<KSelect.ItemIndicator>
|
||||||
@@ -147,11 +161,11 @@ export const Select = (props: SelectProps) => {
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
in="Select-item-label"
|
in="Select-item-label"
|
||||||
>
|
>
|
||||||
{props.noOptionsText || "No options available"}
|
{rest.noOptionsText}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Show when={props.placeholder}>
|
<Show when={root.placeholder}>
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="label"
|
hierarchy="label"
|
||||||
size="s"
|
size="s"
|
||||||
@@ -159,19 +173,22 @@ export const Select = (props: SelectProps) => {
|
|||||||
family="condensed"
|
family="condensed"
|
||||||
in="Select-item-label"
|
in="Select-item-label"
|
||||||
>
|
>
|
||||||
{props.placeholder}
|
{root.placeholder}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Orienter orientation={props.orientation || "horizontal"}>
|
<Orienter orientation={rest.orientation}>
|
||||||
<Label
|
<Label
|
||||||
{...props.label}
|
{...rest.label}
|
||||||
labelComponent={KSelect.Label}
|
labelComponent={KSelect.Label}
|
||||||
descriptionComponent={KSelect.Description}
|
descriptionComponent={KSelect.Description}
|
||||||
validationState={props.error ? "invalid" : "valid"}
|
validationState={rest.error ? "invalid" : "valid"}
|
||||||
|
in={keepTruthy(
|
||||||
|
rest.orientation == "horizontal" && "Orienter-horizontal",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<KSelect.HiddenSelect {...selectProps} />
|
<KSelect.HiddenSelect {...selectProps} />
|
||||||
<KSelect.Trigger
|
<KSelect.Trigger
|
||||||
@@ -201,9 +218,9 @@ export const Select = (props: SelectProps) => {
|
|||||||
</KSelect.Icon>
|
</KSelect.Icon>
|
||||||
</KSelect.Trigger>
|
</KSelect.Trigger>
|
||||||
</Orienter>
|
</Orienter>
|
||||||
<KSelect.Portal mount={defaultMount} {...props.portalProps}>
|
<KSelect.Portal mount={defaultMount} {...rest.portalProps}>
|
||||||
<KSelect.Content
|
<KSelect.Content
|
||||||
class={styles.options_content}
|
class={styles.optionsContent}
|
||||||
style={{ "--z-index": zIndex() }}
|
style={{ "--z-index": zIndex() }}
|
||||||
>
|
>
|
||||||
<KSelect.Listbox>
|
<KSelect.Listbox>
|
||||||
@@ -238,7 +255,7 @@ export const Select = (props: SelectProps) => {
|
|||||||
</KSelect.Content>
|
</KSelect.Content>
|
||||||
</KSelect.Portal>
|
</KSelect.Portal>
|
||||||
{/* TODO: Display error next to the problem */}
|
{/* TODO: Display error next to the problem */}
|
||||||
{/* <KSelect.ErrorMessage>{props.error}</KSelect.ErrorMessage> */}
|
{/* <KSelect.ErrorMessage>{rest.error}</KSelect.ErrorMessage> */}
|
||||||
</KSelect>
|
</KSelect>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user