mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-12 16:15:02 +03:00
f4030c889c
Expose selectable options and selected values, associate validation guidance with controls, and contain keyboard focus in inbound dialogs. Preserve create and edit serialization and add Chromium keyboard and accessibility checks.
114 lines
3.6 KiB
TypeScript
114 lines
3.6 KiB
TypeScript
import { cloneElement, useId } from 'react';
|
|
import type { AriaAttributes, CSSProperties, ReactElement, ReactNode } from 'react';
|
|
import { Controller } from 'react-hook-form';
|
|
import type { Control, ControllerProps, FieldValues, Path } from 'react-hook-form';
|
|
import { Form } from 'antd';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { normalizeAntdOnChange, type ValueProp } from './normalizeAntdOnChange';
|
|
import { toDotted, type FieldName } from './toDotted';
|
|
|
|
interface FormFieldTransform {
|
|
input?: (value: unknown) => unknown;
|
|
output?: (eventValue: unknown) => unknown;
|
|
}
|
|
|
|
export interface FormFieldProps<T extends FieldValues = FieldValues> {
|
|
name: FieldName;
|
|
control?: Control<T>;
|
|
label?: ReactNode;
|
|
tooltip?: ReactNode;
|
|
extra?: ReactNode;
|
|
valueProp?: ValueProp;
|
|
transform?: FormFieldTransform;
|
|
onAfterChange?: (value: unknown) => void;
|
|
rules?: ControllerProps<T>['rules'];
|
|
required?: boolean;
|
|
noStyle?: boolean;
|
|
className?: string;
|
|
style?: CSSProperties;
|
|
children: ReactElement;
|
|
}
|
|
|
|
export function FormField<T extends FieldValues = FieldValues>({
|
|
name,
|
|
control,
|
|
label,
|
|
tooltip,
|
|
extra,
|
|
valueProp = 'value',
|
|
transform,
|
|
onAfterChange,
|
|
rules,
|
|
required,
|
|
noStyle,
|
|
className,
|
|
style,
|
|
children,
|
|
}: FormFieldProps<T>) {
|
|
const { t } = useTranslation();
|
|
const dottedName = toDotted(name) as Path<T>;
|
|
const generatedId = useId();
|
|
const hintId = useId();
|
|
const errorId = `${generatedId}-error`;
|
|
const extraId = `${generatedId}-extra`;
|
|
const childOwnProps = (children as ReactElement<{ id?: string } & AriaAttributes>).props;
|
|
const fieldId = childOwnProps.id ?? generatedId;
|
|
|
|
return (
|
|
<Controller
|
|
control={control}
|
|
name={dottedName}
|
|
rules={rules}
|
|
render={({ field, fieldState }) => {
|
|
const displayValue = transform?.input ? transform.input(field.value) : field.value;
|
|
const help = fieldState.error?.message
|
|
? t(fieldState.error.message, { defaultValue: fieldState.error.message })
|
|
: undefined;
|
|
const describedBy =
|
|
[childOwnProps['aria-describedby'], tooltip && hintId, extra && extraId, help && errorId]
|
|
.filter(Boolean)
|
|
.join(' ') || undefined;
|
|
const childProps: Record<string, unknown> = {
|
|
id: fieldId,
|
|
'aria-describedby': describedBy,
|
|
'aria-invalid': fieldState.invalid || childOwnProps['aria-invalid'],
|
|
[valueProp]: displayValue,
|
|
onChange: (...args: unknown[]) => {
|
|
const raw = normalizeAntdOnChange(args, valueProp);
|
|
const next = transform?.output ? transform.output(raw) : raw;
|
|
field.onChange(next);
|
|
onAfterChange?.(next);
|
|
},
|
|
onBlur: field.onBlur,
|
|
ref: field.ref,
|
|
};
|
|
return (
|
|
<Form.Item
|
|
label={label}
|
|
htmlFor={label ? fieldId : undefined}
|
|
tooltip={tooltip}
|
|
extra={extra ? <span id={extraId}>{extra}</span> : extra}
|
|
required={required}
|
|
validateStatus={fieldState.error ? 'error' : undefined}
|
|
help={help ? <span id={errorId}>{help}</span> : undefined}
|
|
noStyle={noStyle}
|
|
className={className}
|
|
style={style}
|
|
>
|
|
{cloneElement(children as ReactElement<Record<string, unknown>>, childProps)}
|
|
{/* The tooltip only opens on hover, so screen readers get its text as a description. */}
|
|
{tooltip && (
|
|
<span id={hintId} hidden>
|
|
{tooltip}
|
|
</span>
|
|
)}
|
|
</Form.Item>
|
|
);
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export default FormField;
|