commit 987c556dcf4fe5fd118f144cf01d172890fc4426
parent 94246810ebbc4e8f97d89bfce9af22fb719e7b69
Author: Sebastian <sebasjm@gmail.com>
Date: Wed, 29 Jan 2025 15:24:27 -0300
update gls form
Diffstat:
9 files changed, 88 insertions(+), 55 deletions(-)
diff --git a/packages/web-util/src/components/utils.ts b/packages/web-util/src/components/utils.ts
@@ -1,5 +1,5 @@
-import { createElement, VNode } from "preact";
-import { useEffect, useRef } from "preact/hooks";
+import { createElement, Ref, VNode } from "preact";
+import { MutableRef, useEffect, useRef } from "preact/hooks";
export type StateFunc<S> = (p: S) => VNode;
@@ -107,6 +107,52 @@ export function preconnectAs(pre: Preconnect[]) {
}
}
+export function composeRef<T>(...fn: ((e: T | null) => void)[]) {
+ return (element: T | null) => {
+ fn.forEach((handler) => {
+ handler(element);
+ });
+ };
+}
+
+export function saveRef<T>(ref: MutableRef<T>) {
+ return (element: T | null) => {
+ if (element) {
+ ref.current = element;
+ }
+ };
+}
+/**
+ * Show the element when the load ended
+ * @param element
+ */
+export function doAutoFocus<T extends HTMLElement>(
+ element: T | null | undefined,
+) {
+ if (element) {
+ setTimeout(() => {
+ element.focus({ preventScroll: true });
+ }, 100);
+ }
+}
+
+/**
+ * Show the element when the load ended
+ * @param element
+ */
+export function doAutoFocusWithScroll(element: HTMLElement | null) {
+ if (element) {
+ setTimeout(() => {
+ element.focus({ preventScroll: true });
+ element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ inline: "center",
+ });
+ }, 100);
+ }
+}
+
/**
*
* @param obj VNode
diff --git a/packages/web-util/src/forms/FormProvider.tsx b/packages/web-util/src/forms/FormProvider.tsx
@@ -54,7 +54,8 @@ export type FieldUIOptions = {
disabled?: boolean;
/* should not show */
hidden?: boolean;
-
+ /* should start with focus */
+ focus?: boolean;
/* show a mark as required*/
required?: boolean;
};
diff --git a/packages/web-util/src/forms/fields/InputAbsoluteTime.tsx b/packages/web-util/src/forms/fields/InputAbsoluteTime.tsx
@@ -20,6 +20,7 @@ export function InputAbsoluteTime<T extends object, K extends keyof T>(
<Fragment>
<InputLine<T, K>
type="text"
+ focus={properties.focus}
after={{
type: "button",
onClick: () => {
diff --git a/packages/web-util/src/forms/fields/InputArray.tsx b/packages/web-util/src/forms/fields/InputArray.tsx
@@ -46,10 +46,6 @@ function ArrayForm({
selected ?? {},
);
- // useEffect(() => {
- // onChange(form.status.result);
- // }, [form.status.result]);
-
return (
<div class="px-4 py-6">
<div class="grid grid-cols-1 gap-y-8 ">
diff --git a/packages/web-util/src/forms/fields/InputLine.tsx b/packages/web-util/src/forms/fields/InputLine.tsx
@@ -3,6 +3,7 @@ import { ComponentChildren, Fragment, VNode, h } from "preact";
import { Addon, UIFormProps } from "../FormProvider.js";
import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
import { useEffect, useRef } from "preact/hooks";
+import { composeRef, doAutoFocus, saveRef } from "../../components/utils.js";
//@ts-ignore
const TooltipIcon = (
@@ -161,7 +162,7 @@ export function InputLine<T extends object, K extends keyof T>(
props: { type: InputType } & UIFormProps<T, K>,
): VNode {
const { name, placeholder, before, after, converter, type, disabled } = props;
- const input = useRef<HTMLTextAreaElement & HTMLInputElement>(null);
+ const input = useRef<HTMLTextAreaElement | HTMLInputElement>();
const { value, onChange, state, error } =
props.handler ?? noHandlerPropsAndNoContextForField(props.name);
@@ -231,7 +232,7 @@ export function InputLine<T extends object, K extends keyof T>(
>
<textarea
rows={4}
- ref={input}
+ ref={composeRef(saveRef(input), doAutoFocus)}
name={String(name)}
onChange={(e) => {
onChange(fromString(e.currentTarget.value));
@@ -257,7 +258,7 @@ export function InputLine<T extends object, K extends keyof T>(
>
<input
name={String(name)}
- ref={input}
+ ref={composeRef(saveRef(input), doAutoFocus)}
type={type}
onChange={(e) => {
onChange(fromString(e.currentTarget.value));
diff --git a/packages/web-util/src/forms/fields/InputSelectOne.stories.tsx b/packages/web-util/src/forms/fields/InputSelectOne.stories.tsx
@@ -110,6 +110,6 @@ const design2: FormDesign = {
};
export const SimpleRequired = tests.createExample(TestedComponent, {
- initial,
+ initial: {},
design: design2,
});
diff --git a/packages/web-util/src/forms/fields/InputSelectOne.tsx b/packages/web-util/src/forms/fields/InputSelectOne.tsx
@@ -15,6 +15,7 @@ export function InputSelectOne<T extends object, K extends keyof T>(
props.handler ?? noHandlerPropsAndNoContextForField(props.name);
const [filter, setFilter] = useState<string | undefined>(undefined);
+ const [dirty, setDirty] = useState<boolean>();
const regex = new RegExp(`.*${filter}.*`, "i");
const choiceMap = choices.reduce(
(prev, curr) => {
@@ -43,6 +44,7 @@ export function InputSelectOne<T extends object, K extends keyof T>(
type="button"
onClick={() => {
onChange(undefined!);
+ setDirty(true);
}}
class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20"
>
@@ -63,6 +65,7 @@ export function InputSelectOne<T extends object, K extends keyof T>(
value={filter ?? ""}
onChange={(e) => {
setFilter(e.currentTarget.value);
+ setDirty(true);
}}
placeholder={placeholder}
class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
@@ -74,6 +77,7 @@ export function InputSelectOne<T extends object, K extends keyof T>(
type="button"
onClick={() => {
setFilter(filter === undefined ? "" : undefined);
+ setDirty(true);
}}
class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
>
@@ -107,34 +111,18 @@ export function InputSelectOne<T extends object, K extends keyof T>(
onClick={() => {
setFilter(undefined);
onChange(v.value as any);
+ setDirty(true);
}}
-
- // tabindex="-1"
>
- {/* <!-- Selected: "font-semibold" --> */}
<span class="block truncate">{v.label}</span>
-
- {/* <!--
- Checkmark, only display for selected option.
-
- Active: "text-white", Not Active: "text-indigo-600"
- --> */}
</li>
);
})}
-
- {/* <!--
- Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation.
-
- Active: "text-white bg-indigo-600", Not Active: "text-gray-900"
- --> */}
-
- {/* <!-- More items... --> */}
</ul>
)}
</div>
)}
- {error && (
+ {dirty !== undefined && error && (
<p class="mt-2 text-sm text-red-600" id="email-error">
{error}
</p>
diff --git a/packages/web-util/src/forms/forms-ui.tsx b/packages/web-util/src/forms/forms-ui.tsx
@@ -50,23 +50,33 @@ export function DefaultForm<T>({
export function FormUI<T>({
design,
handler,
+ focus,
}: {
design: FormDesign;
handler: FormHandler<T>;
+ focus?: boolean;
}): VNode {
switch (design.type) {
case "double-column": {
const ui = design.sections.map((section, i) => {
if (!section) return <Fragment />;
return (
- <DoubleColumnFormSectionUI section={section} handler={handler} />
+ <DoubleColumnFormSectionUI
+ section={section}
+ handler={handler}
+ focus={focus}
+ />
);
});
return <Fragment>{ui}</Fragment>;
}
case "single-column": {
return (
- <SingleColumnFormSectionUI fields={design.fields} handler={handler} />
+ <SingleColumnFormSectionUI
+ fields={design.fields}
+ handler={handler}
+ focus={focus}
+ />
);
}
}
@@ -74,10 +84,12 @@ export function FormUI<T>({
export function DoubleColumnFormSectionUI<T>({
section,
+ focus,
handler,
}: {
handler: FormHandler<T>;
section: DoubleColumnFormSection;
+ focus?: boolean;
}): VNode {
const { i18n } = useTranslationContext();
return (
@@ -97,6 +109,7 @@ export function DoubleColumnFormSectionUI<T>({
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<RenderAllFieldsByUiConfig
fields={convertFormConfigToUiField(i18n, section.fields, handler)}
+ focus={focus}
/>
</div>
</div>
@@ -107,9 +120,11 @@ export function DoubleColumnFormSectionUI<T>({
export function SingleColumnFormSectionUI<T>({
fields,
handler,
+ focus,
}: {
handler: FormHandler<T>;
fields: UIFormElementConfig[];
+ focus?: boolean;
}): VNode {
const { i18n } = useTranslationContext();
return (
@@ -118,6 +133,7 @@ export function SingleColumnFormSectionUI<T>({
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<RenderAllFieldsByUiConfig
fields={convertFormConfigToUiField(i18n, fields, handler)}
+ focus={focus}
/>
</div>
</div>
@@ -126,9 +142,11 @@ export function SingleColumnFormSectionUI<T>({
}
export function RenderAllFieldsByUiConfig({
+ focus,
fields,
}: {
fields: UIFormField[];
+ focus?: boolean;
}): VNode {
return create(
Fragment,
@@ -137,7 +155,8 @@ export function RenderAllFieldsByUiConfig({
const Component = UIFormConfiguration[
field.type
] as FieldComponentFunction<any>;
- return Component(field.properties);
+ const p = { ...field.properties, focus: focus };
+ return Component(p);
}),
);
}
diff --git a/packages/web-util/src/forms/gana/GLS_Onboarding.ts b/packages/web-util/src/forms/gana/GLS_Onboarding.ts
@@ -14,10 +14,12 @@ export function GLS_Onboarding(
sections: [
{
title: i18n.str`Personal individual information`,
+ description: i18n.str`Contact information of the company representative`,
fields: [
{
id: "PERSON_FULL_NAME" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId,
- label: i18n.str`Full name`,
+ label: i18n.str`First name(s)`,
+ help: i18n.str`As on your ID document`,
// gana_type: "String",
type: "text",
},
@@ -29,20 +31,6 @@ export function GLS_Onboarding(
required: true,
},
{
- id: "PERSON_NATIONAL_ID" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId,
- label: i18n.str`National ID Number`,
- // gana_type: "String",
- type: "text",
- required: true,
- },
- {
- id: "PERSON_NATIONAL_ID_SCAN" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId,
- label: i18n.str`National ID Photo`,
- // gana_type: "File",
- type: "file",
- required: true,
- },
- {
id: "PERSON_DATE_OF_BIRTH" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId,
label: i18n.str`Date of birth`,
// gana_type: "AbsoluteDate",
@@ -172,16 +160,9 @@ export function GLS_Onboarding(
],
},
{
- title: i18n.str`Contact information`,
+ title: i18n.str`Contact information of company headquarters`,
fields: [
{
- id: "CONTACT_DNS_DOMAIN" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId,
- label: i18n.str`Hostname`,
- // gana_type: "Hostname",
- type: "text",
- required: true,
- },
- {
id: "CONTACT_WEB_DOMAIN" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId,
label: i18n.str`Web site`,
// gana_type: "HttpHostnamePath",