Merge pull request 'feat(ui): refine input to allow start and end components' (#5080) from ui/password-input-reveal into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5080
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 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 {
|
||||
@@ -101,7 +114,7 @@ div.form-field {
|
||||
}
|
||||
|
||||
& > .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 cx from "classnames";
|
||||
import { TextInput, TextInputProps } from "@/src/components/Form/TextInput";
|
||||
import Icon from "../Icon/Icon";
|
||||
import { Button } from "@kobalte/core/button";
|
||||
|
||||
const Examples = (props: TextInputProps) => (
|
||||
<div class="flex flex-col gap-8">
|
||||
@@ -83,16 +85,38 @@ export const Tooltip: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Icon: Story = {
|
||||
export const WithIcon: Story = {
|
||||
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 = {
|
||||
args: {
|
||||
...Icon.args,
|
||||
...WithIcon.args,
|
||||
ghost: true,
|
||||
},
|
||||
};
|
||||
@@ -106,14 +130,14 @@ export const Invalid: Story = {
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
...Icon.args,
|
||||
...WithIcon.args,
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const ReadOnly: Story = {
|
||||
args: {
|
||||
...Icon.args,
|
||||
...WithIcon.args,
|
||||
readOnly: true,
|
||||
defaultValue: "14/05/02",
|
||||
},
|
||||
|
||||
@@ -11,12 +11,20 @@ import "./TextInput.css";
|
||||
import { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import { FieldProps } from "./Field";
|
||||
import { Orienter } from "./Orienter";
|
||||
import { splitProps } from "solid-js";
|
||||
import {
|
||||
Component,
|
||||
createEffect,
|
||||
createSignal,
|
||||
onMount,
|
||||
splitProps,
|
||||
} from "solid-js";
|
||||
|
||||
export type TextInputProps = FieldProps &
|
||||
TextFieldRootProps & {
|
||||
icon?: IconVariant;
|
||||
input?: PolymorphicProps<"input", TextFieldInputProps<"input">>;
|
||||
startComponent?: Component<Pick<FieldProps, "inverted">>;
|
||||
endComponent?: Component<Pick<FieldProps, "inverted">>;
|
||||
};
|
||||
|
||||
export const TextInput = (props: TextInputProps) => {
|
||||
@@ -28,6 +36,39 @@ export const TextInput = (props: TextInputProps) => {
|
||||
"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 (
|
||||
<TextField
|
||||
class={cx(
|
||||
@@ -50,6 +91,11 @@ export const TextInput = (props: TextInputProps) => {
|
||||
{...props}
|
||||
/>
|
||||
<div class="input-container">
|
||||
{props.startComponent && !props.readOnly && (
|
||||
<div ref={startComponentRef} class="start-component">
|
||||
{props.startComponent({ inverted: props.inverted })}
|
||||
</div>
|
||||
)}
|
||||
{props.icon && !props.readOnly && (
|
||||
<Icon
|
||||
icon={props.icon}
|
||||
@@ -58,9 +104,17 @@ export const TextInput = (props: TextInputProps) => {
|
||||
/>
|
||||
)}
|
||||
<TextField.Input
|
||||
ref={inputRef}
|
||||
{...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>
|
||||
</Orienter>
|
||||
</TextField>
|
||||
|
||||
@@ -75,7 +75,7 @@ const mockFetcher: Fetcher = <K extends OperationNames>(
|
||||
{
|
||||
name: "gritty.foo",
|
||||
description: "Name of the gritty",
|
||||
prompt_type: "line",
|
||||
prompt_type: "hidden",
|
||||
display: {
|
||||
helperText: null,
|
||||
label: "(2) Password",
|
||||
@@ -113,7 +113,7 @@ const mockFetcher: Fetcher = <K extends OperationNames>(
|
||||
{
|
||||
name: "gritty.foo",
|
||||
description: "Name of the gritty",
|
||||
prompt_type: "line",
|
||||
prompt_type: "hidden",
|
||||
display: {
|
||||
helperText: null,
|
||||
label: "(5) Password",
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from "../InstallMachine";
|
||||
import { TextInput } from "@/src/components/Form/TextInput";
|
||||
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 { Orienter } from "@/src/components/Form/Orienter";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
@@ -35,6 +35,7 @@ import { useClanURI } from "@/src/hooks/clan";
|
||||
import { useApiClient } from "@/src/hooks/ApiClient";
|
||||
import { ProcessMessage, useNotifyOrigin } from "@/src/hooks/notify";
|
||||
import { Loader } from "@/src/components/Loader/Loader";
|
||||
import { Button as KButton } from "@kobalte/core/button";
|
||||
|
||||
export const InstallHeader = (props: { machineName: string }) => {
|
||||
return (
|
||||
@@ -566,35 +567,68 @@ const PromptsFields = (props: PromptsFieldsProps) => {
|
||||
<Field
|
||||
name={`promptValues.${fieldInfo.generator}.${fieldInfo.prompt.name}`}
|
||||
>
|
||||
{(f, props) => (
|
||||
<TextInput
|
||||
{...f}
|
||||
label={
|
||||
fieldInfo.prompt.display?.label ||
|
||||
fieldInfo.prompt.name
|
||||
}
|
||||
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: fieldInfo.prompt.prompt_type.includes(
|
||||
"hidden",
|
||||
)
|
||||
? "password"
|
||||
: "text",
|
||||
...props,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(f, props) => {
|
||||
const defaultInputType =
|
||||
fieldInfo.prompt.prompt_type.includes("hidden")
|
||||
? "password"
|
||||
: "text";
|
||||
|
||||
const [inputType, setInputType] =
|
||||
createSignal(defaultInputType);
|
||||
|
||||
let endComponent:
|
||||
| ((props: { inverted?: boolean }) => JSX.Element)
|
||||
| undefined = undefined;
|
||||
|
||||
if (defaultInputType === "password") {
|
||||
endComponent = (props) => (
|
||||
<KButton
|
||||
onClick={() => {
|
||||
setInputType((type) =>
|
||||
type === "password" ? "text" : "password",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon={
|
||||
inputType() == "password"
|
||||
? "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>
|
||||
)}
|
||||
</For>
|
||||
|
||||
Reference in New Issue
Block a user