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