feat(ui): refine input to allow start and end components
This commit is contained in:
@@ -76,6 +76,19 @@ div.form-field {
|
|||||||
@apply absolute left-2 top-1/2 transform -translate-y-1/2;
|
@apply absolute left-2 top-1/2 transform -translate-y-1/2;
|
||||||
@apply w-[0.875rem] h-[0.875rem] pointer-events-none;
|
@apply w-[0.875rem] h-[0.875rem] pointer-events-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > .start-component {
|
||||||
|
@apply absolute left-2 top-1/2 transform -translate-y-1/2;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .end-component {
|
||||||
|
@apply absolute right-2 top-1/2 transform -translate-y-1/2;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .start-component,
|
||||||
|
& > .end-component {
|
||||||
|
@apply size-fit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.s {
|
&.s {
|
||||||
@@ -101,7 +114,7 @@ div.form-field {
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > .icon {
|
& > .icon {
|
||||||
@apply w-[0.6875rem] h-[0.6875rem] transform -translate-y-1/2;
|
@apply w-[0.6875rem] h-[0.6875rem];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
|
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { TextInput, TextInputProps } from "@/src/components/Form/TextInput";
|
import { TextInput, TextInputProps } from "@/src/components/Form/TextInput";
|
||||||
|
import Icon from "../Icon/Icon";
|
||||||
|
import { Button } from "@kobalte/core/button";
|
||||||
|
|
||||||
const Examples = (props: TextInputProps) => (
|
const Examples = (props: TextInputProps) => (
|
||||||
<div class="flex flex-col gap-8">
|
<div class="flex flex-col gap-8">
|
||||||
@@ -83,16 +85,38 @@ export const Tooltip: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Icon: Story = {
|
export const WithIcon: Story = {
|
||||||
args: {
|
args: {
|
||||||
...Tooltip.args,
|
...Tooltip.args,
|
||||||
icon: "Checkmark",
|
startComponent: () => <Icon icon="EyeClose" color="quaternary" inverted />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithStartComponent: Story = {
|
||||||
|
args: {
|
||||||
|
...Tooltip.args,
|
||||||
|
startComponent: (props: { inverted?: boolean }) => (
|
||||||
|
<Button>
|
||||||
|
<Icon icon="EyeClose" color="quaternary" {...props} />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithEndComponent: Story = {
|
||||||
|
args: {
|
||||||
|
...Tooltip.args,
|
||||||
|
endComponent: (props: { inverted?: boolean }) => (
|
||||||
|
<Button>
|
||||||
|
<Icon icon="EyeOpen" color="quaternary" {...props} />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Ghost: Story = {
|
export const Ghost: Story = {
|
||||||
args: {
|
args: {
|
||||||
...Icon.args,
|
...WithIcon.args,
|
||||||
ghost: true,
|
ghost: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -106,14 +130,14 @@ export const Invalid: Story = {
|
|||||||
|
|
||||||
export const Disabled: Story = {
|
export const Disabled: Story = {
|
||||||
args: {
|
args: {
|
||||||
...Icon.args,
|
...WithIcon.args,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReadOnly: Story = {
|
export const ReadOnly: Story = {
|
||||||
args: {
|
args: {
|
||||||
...Icon.args,
|
...WithIcon.args,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
defaultValue: "14/05/02",
|
defaultValue: "14/05/02",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,12 +11,20 @@ import "./TextInput.css";
|
|||||||
import { PolymorphicProps } from "@kobalte/core/polymorphic";
|
import { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||||
import { FieldProps } from "./Field";
|
import { FieldProps } from "./Field";
|
||||||
import { Orienter } from "./Orienter";
|
import { Orienter } from "./Orienter";
|
||||||
import { splitProps } from "solid-js";
|
import {
|
||||||
|
Component,
|
||||||
|
createEffect,
|
||||||
|
createSignal,
|
||||||
|
onMount,
|
||||||
|
splitProps,
|
||||||
|
} from "solid-js";
|
||||||
|
|
||||||
export type TextInputProps = FieldProps &
|
export type TextInputProps = FieldProps &
|
||||||
TextFieldRootProps & {
|
TextFieldRootProps & {
|
||||||
icon?: IconVariant;
|
icon?: IconVariant;
|
||||||
input?: PolymorphicProps<"input", TextFieldInputProps<"input">>;
|
input?: PolymorphicProps<"input", TextFieldInputProps<"input">>;
|
||||||
|
startComponent?: Component<Pick<FieldProps, "inverted">>;
|
||||||
|
endComponent?: Component<Pick<FieldProps, "inverted">>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TextInput = (props: TextInputProps) => {
|
export const TextInput = (props: TextInputProps) => {
|
||||||
@@ -28,6 +36,39 @@ export const TextInput = (props: TextInputProps) => {
|
|||||||
"ghost",
|
"ghost",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
let inputRef: HTMLInputElement | undefined;
|
||||||
|
let startComponentRef: HTMLDivElement | undefined;
|
||||||
|
let endComponentRef: HTMLDivElement | undefined;
|
||||||
|
|
||||||
|
const [startComponentSize, setStartComponentSize] = createSignal({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
const [endComponentSize, setEndComponentSize] = createSignal({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (startComponentRef) {
|
||||||
|
const rect = startComponentRef.getBoundingClientRect();
|
||||||
|
setStartComponentSize({ width: rect.width, height: rect.height });
|
||||||
|
}
|
||||||
|
if (endComponentRef) {
|
||||||
|
const rect = endComponentRef.getBoundingClientRect();
|
||||||
|
setEndComponentSize({ width: rect.width, height: rect.height });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (inputRef) {
|
||||||
|
const padding = props.size == "s" ? 6 : 8;
|
||||||
|
|
||||||
|
inputRef.style.paddingLeft = `${startComponentSize().width + padding * 2}px`;
|
||||||
|
inputRef.style.paddingRight = `${endComponentSize().width + padding * 2}px`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
class={cx(
|
class={cx(
|
||||||
@@ -50,6 +91,11 @@ export const TextInput = (props: TextInputProps) => {
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
|
{props.startComponent && !props.readOnly && (
|
||||||
|
<div ref={startComponentRef} class="start-component">
|
||||||
|
{props.startComponent({ inverted: props.inverted })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{props.icon && !props.readOnly && (
|
{props.icon && !props.readOnly && (
|
||||||
<Icon
|
<Icon
|
||||||
icon={props.icon}
|
icon={props.icon}
|
||||||
@@ -58,9 +104,17 @@ export const TextInput = (props: TextInputProps) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
|
ref={inputRef}
|
||||||
{...props.input}
|
{...props.input}
|
||||||
classList={{ "has-icon": props.icon && !props.readOnly }}
|
class={cx({
|
||||||
|
"has-icon": props.icon && !props.readOnly,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
|
{props.endComponent && !props.readOnly && (
|
||||||
|
<div ref={endComponentRef} class="end-component">
|
||||||
|
{props.endComponent({ inverted: props.inverted })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Orienter>
|
</Orienter>
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ const mockFetcher: Fetcher = <K extends OperationNames>(
|
|||||||
{
|
{
|
||||||
name: "gritty.foo",
|
name: "gritty.foo",
|
||||||
description: "Name of the gritty",
|
description: "Name of the gritty",
|
||||||
prompt_type: "line",
|
prompt_type: "hidden",
|
||||||
display: {
|
display: {
|
||||||
helperText: null,
|
helperText: null,
|
||||||
label: "(2) Password",
|
label: "(2) Password",
|
||||||
@@ -113,7 +113,7 @@ const mockFetcher: Fetcher = <K extends OperationNames>(
|
|||||||
{
|
{
|
||||||
name: "gritty.foo",
|
name: "gritty.foo",
|
||||||
description: "Name of the gritty",
|
description: "Name of the gritty",
|
||||||
prompt_type: "line",
|
prompt_type: "hidden",
|
||||||
display: {
|
display: {
|
||||||
helperText: null,
|
helperText: null,
|
||||||
label: "(5) Password",
|
label: "(5) Password",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
} from "../InstallMachine";
|
} from "../InstallMachine";
|
||||||
import { TextInput } from "@/src/components/Form/TextInput";
|
import { TextInput } from "@/src/components/Form/TextInput";
|
||||||
import { Alert, AlertProps } from "@/src/components/Alert/Alert";
|
import { Alert, AlertProps } from "@/src/components/Alert/Alert";
|
||||||
import { createSignal, For, Match, Show, Switch } from "solid-js";
|
import { createSignal, For, Match, Show, Switch, JSX } from "solid-js";
|
||||||
import { Divider } from "@/src/components/Divider/Divider";
|
import { Divider } from "@/src/components/Divider/Divider";
|
||||||
import { Orienter } from "@/src/components/Form/Orienter";
|
import { Orienter } from "@/src/components/Form/Orienter";
|
||||||
import { Button } from "@/src/components/Button/Button";
|
import { Button } from "@/src/components/Button/Button";
|
||||||
@@ -35,6 +35,7 @@ import { useClanURI } from "@/src/hooks/clan";
|
|||||||
import { useApiClient } from "@/src/hooks/ApiClient";
|
import { useApiClient } from "@/src/hooks/ApiClient";
|
||||||
import { ProcessMessage, useNotifyOrigin } from "@/src/hooks/notify";
|
import { ProcessMessage, useNotifyOrigin } from "@/src/hooks/notify";
|
||||||
import { Loader } from "@/src/components/Loader/Loader";
|
import { Loader } from "@/src/components/Loader/Loader";
|
||||||
|
import { Button as KButton } from "@kobalte/core/button";
|
||||||
|
|
||||||
export const InstallHeader = (props: { machineName: string }) => {
|
export const InstallHeader = (props: { machineName: string }) => {
|
||||||
return (
|
return (
|
||||||
@@ -566,35 +567,68 @@ const PromptsFields = (props: PromptsFieldsProps) => {
|
|||||||
<Field
|
<Field
|
||||||
name={`promptValues.${fieldInfo.generator}.${fieldInfo.prompt.name}`}
|
name={`promptValues.${fieldInfo.generator}.${fieldInfo.prompt.name}`}
|
||||||
>
|
>
|
||||||
{(f, props) => (
|
{(f, props) => {
|
||||||
<TextInput
|
const defaultInputType =
|
||||||
{...f}
|
fieldInfo.prompt.prompt_type.includes("hidden")
|
||||||
label={
|
? "password"
|
||||||
fieldInfo.prompt.display?.label ||
|
: "text";
|
||||||
fieldInfo.prompt.name
|
|
||||||
}
|
const [inputType, setInputType] =
|
||||||
description={fieldInfo.prompt.description}
|
createSignal(defaultInputType);
|
||||||
value={f.value || fieldInfo.value || ""}
|
|
||||||
required={fieldInfo.prompt.display?.required}
|
let endComponent:
|
||||||
orientation="horizontal"
|
| ((props: { inverted?: boolean }) => JSX.Element)
|
||||||
validationState={
|
| undefined = undefined;
|
||||||
getError(
|
|
||||||
formStore,
|
if (defaultInputType === "password") {
|
||||||
`promptValues.${fieldInfo.generator}.${fieldInfo.prompt.name}`,
|
endComponent = (props) => (
|
||||||
)
|
<KButton
|
||||||
? "invalid"
|
onClick={() => {
|
||||||
: "valid"
|
setInputType((type) =>
|
||||||
}
|
type === "password" ? "text" : "password",
|
||||||
input={{
|
);
|
||||||
type: fieldInfo.prompt.prompt_type.includes(
|
}}
|
||||||
"hidden",
|
>
|
||||||
)
|
<Icon
|
||||||
? "password"
|
icon={
|
||||||
: "text",
|
inputType() == "password"
|
||||||
...props,
|
? "EyeClose"
|
||||||
}}
|
: "EyeOpen"
|
||||||
/>
|
}
|
||||||
)}
|
color="quaternary"
|
||||||
|
inverted={props.inverted}
|
||||||
|
/>
|
||||||
|
</KButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextInput
|
||||||
|
{...f}
|
||||||
|
label={
|
||||||
|
fieldInfo.prompt.display?.label ||
|
||||||
|
fieldInfo.prompt.name
|
||||||
|
}
|
||||||
|
endComponent={endComponent}
|
||||||
|
description={fieldInfo.prompt.description}
|
||||||
|
value={f.value || fieldInfo.value || ""}
|
||||||
|
required={fieldInfo.prompt.display?.required}
|
||||||
|
orientation="horizontal"
|
||||||
|
validationState={
|
||||||
|
getError(
|
||||||
|
formStore,
|
||||||
|
`promptValues.${fieldInfo.generator}.${fieldInfo.prompt.name}`,
|
||||||
|
)
|
||||||
|
? "invalid"
|
||||||
|
: "valid"
|
||||||
|
}
|
||||||
|
input={{
|
||||||
|
type: inputType(),
|
||||||
|
...props,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
Reference in New Issue
Block a user