From fdbe623e1060efc4b074d213a96e8f5a2ab7498b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 20 Sep 2023 15:16:28 -0300 Subject: more ui stuff, moved forms to util --- packages/web-util/src/forms/InputLine.tsx | 282 ++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 packages/web-util/src/forms/InputLine.tsx (limited to 'packages/web-util/src/forms/InputLine.tsx') diff --git a/packages/web-util/src/forms/InputLine.tsx b/packages/web-util/src/forms/InputLine.tsx new file mode 100644 index 000000000..9448ef5e4 --- /dev/null +++ b/packages/web-util/src/forms/InputLine.tsx @@ -0,0 +1,282 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; +import { ComponentChildren, Fragment, VNode, h } from "preact"; +import { useField } from "./useField.js"; + +export interface IconAddon { + type: "icon"; + icon: VNode; +} +interface ButtonAddon { + type: "button"; + onClick: () => void; + children: ComponentChildren; +} +interface TextAddon { + type: "text"; + text: TranslatedString; +} +type Addon = IconAddon | ButtonAddon | TextAddon; + +interface StringConverter { + toStringUI: (v?: T) => string; + fromStringUI: (v?: string) => T; +} + +export interface UIFormProps { + name: K; + label: TranslatedString; + placeholder?: TranslatedString; + tooltip?: TranslatedString; + help?: TranslatedString; + before?: Addon; + after?: Addon; + required?: boolean; + converter?: StringConverter; +} + +export type FormErrors = { + [P in keyof T]?: string | FormErrors; +}; + +//@ts-ignore +const TooltipIcon = ( + + + +); + +export function LabelWithTooltipMaybeRequired({ + label, + required, + tooltip, +}: { + label: TranslatedString; + required?: boolean; + tooltip?: TranslatedString; +}): VNode { + const Label = ( + +
+ +
+
+ ); + const WithTooltip = tooltip ? ( +
+ {Label} + + {TooltipIcon} + + +
+ ) : ( + Label + ); + if (required) { + return ( +
+ {WithTooltip} + * +
+ ); + } + return WithTooltip; +} + +function InputWrapper({ + children, + label, + tooltip, + before, + after, + help, + error, + required, +}: { error?: string; children: ComponentChildren } & UIFormProps): VNode { + return ( +
+ +
+ {before && + (before.type === "text" ? ( + + {before.text} + + ) : before.type === "icon" ? ( +
+ {before.icon} +
+ ) : before.type === "button" ? ( + + ) : undefined)} + + {children} + + {after && + (after.type === "text" ? ( + + {after.text} + + ) : after.type === "icon" ? ( +
+ {after.icon} +
+ ) : after.type === "button" ? ( + + ) : undefined)} +
+ {error && ( +

+ {error} +

+ )} + {help && ( +

+ {help} +

+ )} +
+ ); +} + +function defaultToString(v: unknown) { + return v === undefined ? "" : typeof v !== "object" ? String(v) : ""; +} +function defaultFromString(v: string) { + return v; +} + +type InputType = "text" | "text-area" | "password" | "email" | "number"; + +export function InputLine( + props: { type: InputType } & UIFormProps, +): VNode { + const { name, placeholder, before, after, converter, type } = props; + const { value, onChange, state, isDirty } = useField(name); + + if (state.hidden) return
; + + let clazz = + "block w-full rounded-md border-0 py-1.5 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200"; + if (before) { + switch (before.type) { + case "icon": { + clazz += " pl-10"; + break; + } + case "button": { + clazz += " rounded-none rounded-r-md "; + break; + } + case "text": { + clazz += " min-w-0 flex-1 rounded-r-md rounded-none "; + break; + } + } + } + if (after) { + switch (after.type) { + case "icon": { + clazz += " pr-10"; + break; + } + case "button": { + clazz += " rounded-none rounded-l-md"; + break; + } + case "text": { + clazz += " min-w-0 flex-1 rounded-l-md rounded-none "; + break; + } + } + } + const showError = isDirty && state.error; + if (showError) { + clazz += + " text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500"; + } else { + clazz += + " text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-indigo-600"; + } + const fromString: (s: string) => any = + converter?.fromStringUI ?? defaultFromString; + const toString: (s: any) => string = converter?.toStringUI ?? defaultToString; + + if (type === "text-area") { + return ( + + {...props} + error={showError ? state.error : undefined} + > +