diff options
Diffstat (limited to 'packages/web-util/src/forms/InputLine.tsx')
-rw-r--r-- | packages/web-util/src/forms/InputLine.tsx | 272 |
1 files changed, 272 insertions, 0 deletions
diff --git a/packages/web-util/src/forms/InputLine.tsx b/packages/web-util/src/forms/InputLine.tsx new file mode 100644 index 000000000..eb3238ef9 --- /dev/null +++ b/packages/web-util/src/forms/InputLine.tsx @@ -0,0 +1,272 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; +import { ComponentChildren, Fragment, VNode, h } from "preact"; +import { Addon, UIFormProps } from "./FormProvider.js"; +import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; +import { useField } from "./useField.js"; + +//@ts-ignore +const TooltipIcon = ( + <svg + class="w-5 h-5" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + > + <path + fill-rule="evenodd" + d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" + clip-rule="evenodd" + /> + </svg> +); + +export function LabelWithTooltipMaybeRequired({ + label, + required, + tooltip, +}: { + label: TranslatedString; + required?: boolean; + tooltip?: TranslatedString; +}): VNode { + const Label = ( + <Fragment> + <div class="flex justify-between"> + <label + htmlFor="email" + class="block text-sm font-medium leading-6 text-gray-900" + > + {label} + </label> + </div> + </Fragment> + ); + const WithTooltip = tooltip ? ( + <div class="relative flex flex-grow items-stretch focus-within:z-10"> + {Label} + <span class="relative flex items-center group pl-2"> + {TooltipIcon} + <div class="absolute bottom-0 -ml-10 hidden flex-col items-center mb-6 group-hover:flex w-28"> + <div class="relative z-10 p-2 text-xs leading-none text-white whitespace-no-wrap bg-black shadow-lg"> + {tooltip} + </div> + <div class="w-3 h-3 -mt-2 rotate-45 bg-black"></div> + </div> + </span> + </div> + ) : ( + Label + ); + if (required) { + return ( + <div class="flex justify-between"> + {WithTooltip} + <span class="text-sm leading-6 text-red-600">*</span> + </div> + ); + } + return WithTooltip; +} + +export function RenderAddon({ disabled, addon }: { disabled?: boolean, addon: Addon }): VNode { + switch (addon.type) { + case "text": { + return ( + <span class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm"> + {addon.text} + </span> + ); + } + case "icon": { + return ( + <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> + {addon.icon} + </div> + ); + } + case "button": { + return ( + <button + type="button" + disabled={disabled} + onClick={addon.onClick} + class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-l-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50" + > + {addon.children} + </button> + ); + } + } +} + +function InputWrapper<T extends object, K extends keyof T>({ + children, + label, + tooltip, + before, + after, + help, + error, + disabled, + required, +}: { + error?: string; + disabled: boolean; + children: ComponentChildren; +} & UIFormProps<T, K>): VNode { + return ( + <div class="sm:col-span-6"> + <LabelWithTooltipMaybeRequired + label={label} + required={required} + tooltip={tooltip} + /> + <div class="relative mt-2 flex rounded-md shadow-sm"> + {before && <RenderAddon disabled={disabled} addon={before} />} + + {children} + + {after && <RenderAddon disabled={disabled} addon={after} />} + </div> + {error && ( + <p class="mt-2 text-sm text-red-600" id="email-error"> + {error} + </p> + )} + {help && ( + <p class="mt-2 text-sm text-gray-500" id="email-description"> + {help} + </p> + )} + </div> + ); +} + +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<T extends object, K extends keyof T>( + props: { type: InputType } & UIFormProps<T, K>, +): VNode { + const { name, placeholder, before, after, converter, type } = props; + //FIXME: remove deprecated + const fieldCtx = useField<T, K>(props.name); + const { value, onChange, state, error } = + props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name); + + // const [text, setText] = useState(""); + const fromString: (s: string) => any = + converter?.fromStringUI ?? defaultFromString; + const toString: (s: any) => string = converter?.toStringUI ?? defaultToString; + + // useEffect(() => { + // const newValue = toString(value); + // if (newValue) { + // setText(newValue); + // } + // }, [value]); + + if (state.hidden) return <div />; + + 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 = value !== undefined && 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"; + } + + if (type === "text-area") { + return ( + <InputWrapper<T, K> + {...props} + help={props.help ?? state.help} + disabled={state.disabled ?? false} + error={showError ? error : undefined} + > + <textarea + rows={4} + name={String(name)} + onChange={(e) => { + onChange(fromString(e.currentTarget.value)); + }} + placeholder={placeholder ? placeholder : undefined} + value={toString(value) ?? ""} + // defaultValue={toString(value)} + disabled={state.disabled} + aria-invalid={showError} + // aria-describedby="email-error" + class={clazz} + /> + </InputWrapper> + ); + } + + return ( + <InputWrapper<T, K> + {...props} + help={props.help ?? state.help} + disabled={state.disabled ?? false} + error={showError ? error : undefined} + > + <input + name={String(name)} + type={type} + onChange={(e) => { + onChange(fromString(e.currentTarget.value)); + }} + placeholder={placeholder ? placeholder : undefined} + value={toString(value) ?? ""} + // onBlur={() => { + // onChange(fromString(value as any)); + // }} + // defaultValue={toString(value)} + disabled={state.disabled} + aria-invalid={showError} + // aria-describedby="email-error" + class={clazz} + /> + </InputWrapper> + ); +} |