Done with select tag

This commit is contained in:
Joseph Garrone 2024-05-05 18:51:33 +02:00
parent 41c2685dc4
commit b17724fdda

View File

@ -6,7 +6,7 @@ import {
type KcContextLike, type KcContextLike,
type FormAction, type FormAction,
type FormFieldError, type FormFieldError,
FormFieldState type FormFieldState
} from "keycloakify/login/lib/useUserProfileForm"; } from "keycloakify/login/lib/useUserProfileForm";
import type { Attribute, LegacyAttribute } from "keycloakify/login/kcContext/KcContext"; import type { Attribute, LegacyAttribute } from "keycloakify/login/kcContext/KcContext";
import type { I18n } from "../../i18n"; import type { I18n } from "../../i18n";
@ -23,11 +23,10 @@ export type UserProfileFormFieldsProps = {
type BeforeAfterFieldProps = { type BeforeAfterFieldProps = {
attribute: Attribute; attribute: Attribute;
index: number;
value: string;
dispatchFormAction: React.Dispatch<FormAction>; dispatchFormAction: React.Dispatch<FormAction>;
formFieldErrors: FormFieldError[]; displayableErrors: FormFieldError[];
i18n: I18n; i18n: I18n;
valueOrValues: string | string[];
}; };
// NOTE: Enabled by default but it's a UX best practice to set it to false. // NOTE: Enabled by default but it's a UX best practice to set it to false.
@ -55,14 +54,14 @@ export function UserProfileFormFields(props: UserProfileFormFieldsProps) {
return ( return (
<> <>
{formFieldStates.map(({ index, value, attribute, displayableErrors }) => { {formFieldStates.map(({ attribute, displayableErrors, valueOrValues }) => {
const formGroupClassName = clsx( const formGroupClassName = clsx(
getClassName("kcFormGroupClass"), getClassName("kcFormGroupClass"),
displayableErrors.length !== 0 && getClassName("kcFormGroupErrorClass") displayableErrors.length !== 0 && getClassName("kcFormGroupErrorClass")
); );
return ( return (
<Fragment key={`${attribute.name}-${index}`}> <Fragment key={attribute.name}>
<GroupLabel <GroupLabel
attribute={attribute} attribute={attribute}
getClassName={getClassName} getClassName={getClassName}
@ -73,11 +72,10 @@ export function UserProfileFormFields(props: UserProfileFormFieldsProps) {
{BeforeField !== undefined && ( {BeforeField !== undefined && (
<BeforeField <BeforeField
attribute={attribute} attribute={attribute}
index={index}
value={value}
dispatchFormAction={dispatchFormAction} dispatchFormAction={dispatchFormAction}
formFieldErrors={displayableErrors} displayableErrors={displayableErrors}
i18n={i18n} i18n={i18n}
valueOrValues={valueOrValues}
/> />
)} )}
<div <div
@ -91,7 +89,7 @@ export function UserProfileFormFields(props: UserProfileFormFieldsProps) {
{attribute.required && <>*</>} {attribute.required && <>*</>}
</div> </div>
<div className={getClassName("kcInputWrapperClass")}> <div className={getClassName("kcInputWrapperClass")}>
{attribute.annotations.inputHelperTextBefore !== undefined && index === 0 && ( {attribute.annotations.inputHelperTextBefore !== undefined && (
<div <div
className={getClassName("kcInputHelperTextBeforeClass")} className={getClassName("kcInputHelperTextBeforeClass")}
id={`form-help-text-before-${attribute.name}`} id={`form-help-text-before-${attribute.name}`}
@ -108,7 +106,8 @@ export function UserProfileFormFields(props: UserProfileFormFieldsProps) {
getClassName={getClassName} getClassName={getClassName}
i18n={i18n} i18n={i18n}
/> />
{attribute.multivalued && (
{/*attribute.multivalued && (
<AddRemoveButtonsMultiValuedAttribute <AddRemoveButtonsMultiValuedAttribute
formFieldStates={formFieldStates} formFieldStates={formFieldStates}
attribute={attribute} attribute={attribute}
@ -116,19 +115,19 @@ export function UserProfileFormFields(props: UserProfileFormFieldsProps) {
dispatchFormAction={dispatchFormAction} dispatchFormAction={dispatchFormAction}
i18n={i18n} i18n={i18n}
/> />
)} )*/}
{displayableErrors.length !== 0 && ( {displayableErrors.length !== 0 && (
<FieldErrors <FieldErrors
attribute={attribute} attribute={attribute}
index={index}
getClassName={getClassName} getClassName={getClassName}
displayableErrors={displayableErrors} displayableErrors={displayableErrors}
fieldIndex={undefined}
/> />
)} )}
{attribute.annotations.inputHelperTextAfter !== undefined && index === 0 && ( {attribute.annotations.inputHelperTextAfter !== undefined && (
<div <div
className={getClassName("kcInputHelperTextAfterClass")} className={getClassName("kcInputHelperTextAfterClass")}
id={`form-help-text-before-${attribute.name}`} id={`form-help-text-after-${attribute.name}`}
aria-live="polite" aria-live="polite"
> >
{advancedMsg(attribute.annotations.inputHelperTextAfter)} {advancedMsg(attribute.annotations.inputHelperTextAfter)}
@ -138,11 +137,10 @@ export function UserProfileFormFields(props: UserProfileFormFieldsProps) {
{AfterField !== undefined && ( {AfterField !== undefined && (
<AfterField <AfterField
attribute={attribute} attribute={attribute}
index={index}
value={value}
dispatchFormAction={dispatchFormAction} dispatchFormAction={dispatchFormAction}
formFieldErrors={displayableErrors} displayableErrors={displayableErrors}
i18n={i18n} i18n={i18n}
valueOrValues={valueOrValues}
/> />
)} )}
{/* {/*
@ -254,22 +252,24 @@ function GroupLabel(props: {
function FieldErrors(props: { function FieldErrors(props: {
attribute: Attribute; attribute: Attribute;
index: number;
getClassName: UserProfileFormFieldsProps["getClassName"]; getClassName: UserProfileFormFieldsProps["getClassName"];
displayableErrors: FormFieldError[]; displayableErrors: FormFieldError[];
fieldIndex: number | undefined;
}) { }) {
const { attribute, index, getClassName, displayableErrors } = props; const { attribute, getClassName, displayableErrors, fieldIndex } = props;
return ( return (
<span <span
id={`input-error-${attribute.name}${index === 0 ? "" : `-${index + 1}`}`} id={`input-error-${attribute.name}${fieldIndex === undefined ? "" : `-${fieldIndex}`}`}
className={getClassName("kcInputErrorMessageClass")} className={getClassName("kcInputErrorMessageClass")}
style={{ style={{
"position": displayableErrors.length === 1 ? "absolute" : undefined "position": displayableErrors.length === 1 ? "absolute" : undefined
}} }}
aria-live="polite" aria-live="polite"
> >
{displayableErrors.map(({ errorMessage }, i, arr) => ( {displayableErrors
.filter(error => error.fieldIndex === fieldIndex)
.map(({ errorMessage }, i, arr) => (
<> <>
<span key={i}>{errorMessage}</span> <span key={i}>{errorMessage}</span>
{arr.length - 1 !== i && <br />} {arr.length - 1 !== i && <br />}
@ -398,8 +398,7 @@ function AddRemoveButtonsMultiValuedAttribute(props: {
type PropsOfInputFiledByType = { type PropsOfInputFiledByType = {
attribute: Attribute; attribute: Attribute;
index: number; valueOrValues: string | string[];
value: string;
displayableErrors: FormFieldError[]; displayableErrors: FormFieldError[];
formValidationDispatch: React.Dispatch<FormAction>; formValidationDispatch: React.Dispatch<FormAction>;
getClassName: UserProfileFormFieldsProps["getClassName"]; getClassName: UserProfileFormFieldsProps["getClassName"];
@ -407,7 +406,7 @@ type PropsOfInputFiledByType = {
}; };
function InputFiledByType(props: PropsOfInputFiledByType) { function InputFiledByType(props: PropsOfInputFiledByType) {
const { attribute } = props; const { attribute, valueOrValues } = props;
/* /*
<#macro inputFieldByType attribute> <#macro inputFieldByType attribute>
@ -445,8 +444,22 @@ function InputFiledByType(props: PropsOfInputFiledByType) {
case "multiselect-checkboxes": case "multiselect-checkboxes":
return <InputTagSelects {...props} />; return <InputTagSelects {...props} />;
default: default:
return <InputTag {...props} />; if (valueOrValues instanceof Array) {
return (
<>
{valueOrValues.map((...[, i]) => (
<InputTag key={i} {...props} fieldIndex={i} />
))}
</>
);
} }
return <InputTag {...props} fieldIndex={undefined} />;
}
}
function InputTag(props: PropsOfInputFiledByType & { fieldIndex: number | undefined }) {
return null;
} }
function InputTagSelects(props: PropsOfInputFiledByType) { function InputTagSelects(props: PropsOfInputFiledByType) {
@ -485,31 +498,37 @@ function InputTagSelects(props: PropsOfInputFiledByType) {
</#macro> </#macro>
*/ */
const { attribute, formValidationDispatch, getClassName, index } = props; const { attribute, formValidationDispatch, getClassName, valueOrValues } = props;
const { advancedMsg } = props.i18n; const { advancedMsg } = props.i18n;
const { classDiv, classInput, classLabel, inputType } = const { classDiv, classInput, classLabel, inputType } = (() => {
attribute.annotations.inputType === "select-radiobuttons" const { inputType } = attribute.annotations;
? {
assert(inputType === "select-radiobuttons" || inputType === "multiselect-checkboxes");
switch (inputType) {
case "select-radiobuttons":
return {
"inputType": "radio", "inputType": "radio",
"classDiv": getClassName("kcInputClassRadio"), "classDiv": getClassName("kcInputClassRadio"),
"classInput": getClassName("kcInputClassRadioInput"), "classInput": getClassName("kcInputClassRadioInput"),
"classLabel": getClassName("kcInputClassRadioLabel") "classLabel": getClassName("kcInputClassRadioLabel")
} };
: { case "multiselect-checkboxes":
return {
"inputType": "checkbox", "inputType": "checkbox",
"classDiv": getClassName("kcInputClassCheckbox"), "classDiv": getClassName("kcInputClassCheckbox"),
"classInput": getClassName("kcInputClassCheckboxInput"), "classInput": getClassName("kcInputClassCheckboxInput"),
"classLabel": getClassName("kcInputClassCheckboxLabel") "classLabel": getClassName("kcInputClassCheckboxLabel")
}; };
}
})();
const options = (() => { const options = (() => {
walk: { walk: {
const { inputOptionsFromValidation } = attribute.annotations; const { inputOptionsFromValidation } = attribute.annotations;
assert(typeof inputOptionsFromValidation === "string");
if (inputOptionsFromValidation === undefined) { if (inputOptionsFromValidation === undefined) {
break walk; break walk;
} }
@ -536,26 +555,41 @@ function InputTagSelects(props: PropsOfInputFiledByType) {
<div key={option} className={classDiv}> <div key={option} className={classDiv}>
<input <input
type={inputType} type={inputType}
id={`${attribute.name}-${option}-${index === 0 ? "" : index + 1}`} id={`${attribute.name}-${option}`}
name={attribute.name} name={attribute.name}
value={option} value={option}
className={classInput} className={classInput}
aria-invalid={props.displayableErrors.length !== 0} aria-invalid={props.displayableErrors.length !== 0}
disabled={attribute.readOnly} disabled={attribute.readOnly}
checked={props.value === option} checked={valueOrValues.includes(option)}
onChange={() => onChange={event =>
formValidationDispatch({ formValidationDispatch({
"action": "update value", "action": "update",
"name": attribute.name, "name": attribute.name,
"index": props.index, "valueOrValues": (() => {
"newValue": option const isChecked = event.target.checked;
if (valueOrValues instanceof Array) {
const newValues = [...valueOrValues];
if (isChecked) {
newValues.push(option);
} else {
newValues.splice(newValues.indexOf(option), 1);
}
return newValues;
}
return event.target.checked ? option : "";
})()
}) })
} }
onBlur={() => onBlur={() =>
formValidationDispatch({ formValidationDispatch({
"action": "focus lost", "action": "focus lost",
"name": attribute.name, "name": attribute.name,
"index": props.index "fieldIndex": undefined
}) })
} }
/> />
@ -572,11 +606,15 @@ function InputTagSelects(props: PropsOfInputFiledByType) {
} }
function TextareaTag(props: PropsOfInputFiledByType) { function TextareaTag(props: PropsOfInputFiledByType) {
const { attribute, index, value, formValidationDispatch, getClassName, displayableErrors } = props; const { attribute, formValidationDispatch, getClassName, displayableErrors, valueOrValues } = props;
assert(typeof valueOrValues === "string");
const value = valueOrValues;
return ( return (
<textarea <textarea
id={`${attribute.name}-${index === 0 ? "" : index + 1}`} id={attribute.name}
name={attribute.name} name={attribute.name}
className={getClassName("kcInputClass")} className={getClassName("kcInputClass")}
aria-invalid={displayableErrors.length !== 0} aria-invalid={displayableErrors.length !== 0}
@ -587,17 +625,16 @@ function TextareaTag(props: PropsOfInputFiledByType) {
value={value} value={value}
onChange={event => onChange={event =>
formValidationDispatch({ formValidationDispatch({
"action": "update value", "action": "update",
"name": attribute.name, "name": attribute.name,
index, "valueOrValues": event.target.value
"newValue": event.target.value
}) })
} }
onBlur={() => onBlur={() =>
formValidationDispatch({ formValidationDispatch({
"action": "focus lost", "action": "focus lost",
"name": attribute.name, "name": attribute.name,
index "fieldIndex": undefined
}) })
} }
/> />
@ -605,7 +642,7 @@ function TextareaTag(props: PropsOfInputFiledByType) {
} }
function SelectTag(props: PropsOfInputFiledByType) { function SelectTag(props: PropsOfInputFiledByType) {
const { attribute, index, value, formValidationDispatch, getClassName, displayableErrors, i18n } = props; const { attribute, formValidationDispatch, getClassName, displayableErrors, i18n, valueOrValues } = props;
const { advancedMsg } = i18n; const { advancedMsg } = i18n;
@ -613,31 +650,36 @@ function SelectTag(props: PropsOfInputFiledByType) {
return ( return (
<select <select
id={`${attribute.name}-${index === 0 ? "" : index + 1}`} id={attribute.name}
name={attribute.name} name={attribute.name}
className={getClassName("kcInputClass")} className={getClassName("kcInputClass")}
aria-invalid={displayableErrors.length !== 0} aria-invalid={displayableErrors.length !== 0}
disabled={attribute.readOnly} disabled={attribute.readOnly}
multiple={isMultiple} multiple={isMultiple}
size={attribute.annotations.inputTypeSize === undefined ? undefined : parseInt(attribute.annotations.inputTypeSize)} size={attribute.annotations.inputTypeSize === undefined ? undefined : parseInt(attribute.annotations.inputTypeSize)}
value={value} value={valueOrValues}
onChange={event => onChange={event =>
formValidationDispatch({ formValidationDispatch({
"action": "update value", "action": "update",
"name": attribute.name, "name": attribute.name,
index, "valueOrValues": (() => {
"newValue": event.target.value if (isMultiple) {
return Array.from(event.target.selectedOptions).map(option => option.value);
}
return event.target.value;
})()
}) })
} }
onBlur={() => onBlur={() =>
formValidationDispatch({ formValidationDispatch({
"action": "focus lost", "action": "focus lost",
"name": attribute.name, "name": attribute.name,
index "fieldIndex": undefined
}) })
} }
> >
{attribute.annotations.inputType === "select" && <option value=""></option>} {!isMultiple && <option value=""></option>}
{(() => { {(() => {
const options = (() => { const options = (() => {
walk: { walk: {
@ -666,7 +708,7 @@ function SelectTag(props: PropsOfInputFiledByType) {
})(); })();
return options.map(option => ( return options.map(option => (
<option key={option} value={option} selected={value === option}> <option key={option} value={option}>
{(() => { {(() => {
if (attribute.annotations.inputOptionLabels !== undefined) { if (attribute.annotations.inputOptionLabels !== undefined) {
const { inputOptionLabels } = attribute.annotations; const { inputOptionLabels } = attribute.annotations;