Files
ente/web/packages/base/components/SingleInputForm.tsx
Manav Rathi 8682e3338b Rename
2025-05-30 17:40:51 +05:30

194 lines
6.6 KiB
TypeScript

import {
Stack,
TextField,
type ButtonProps,
type TextFieldProps,
} from "@mui/material";
import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton";
import { LoadingButton } from "ente-base/components/mui/LoadingButton";
import log from "ente-base/log";
import { useFormik } from "formik";
import { t } from "i18next";
import React, { useCallback, useState } from "react";
import { ShowHidePasswordInputAdornment } from "./mui/PasswordInputAdornment";
export type SingleInputFormProps = Pick<
TextFieldProps,
"label" | "placeholder" | "autoComplete" | "autoFocus" | "slotProps"
> & {
/**
* The type attribute of the HTML input element that will be used.
*
* Default is "text".
*
* In addition to changing the behaviour of the HTML input element, the
* {@link SingleInputForm} component also has special casing for type
* "password", wherein it'll show an adornment at the end of the text field
* allowing the user to show or hide the password.
*/
inputType?: TextFieldProps["type"];
/**
* The initial value, if any, to prefill in the input.
*/
initialValue?: string;
/**
* Title for the submit button.
*/
submitButtonTitle: string;
/**
* Color of the submit button.
*
* Default: "primary".
*/
submitButtonColor?: ButtonProps["color"];
/**
* Cancellation handler.
*
* This function is called when the user activates the cancel button in the
* form.
*
* If this is not provided, then only a full width submit button will be
* shown in the form (below the input).
*/
onCancel?: () => void;
/**
* Submission handler. A callback invoked when the submit button is pressed.
*
* During submission, the text input and the submit button are disabled, and
* an indeterminate progress indicator is shown.
*
* If this function rejects then a generic error helper text is shown below
* the text input, and the input (/ buttons) reenabled.
*
* This function is also passed an function that can be used to explicitly
* set the error message that as shown below the text input when a specific
* problem occurs during submission.
*
* @param name The current value of the text input.
*
* @param setFieldError A function that can be called to set the error message
* shown below the text input if submission fails.
*
* Note that if {@link setFieldError} is called, then the {@link onSubmit}
* function should not throw, otherwise the error message shown by
* {@link setFieldError} will get overwritten by the generic error message.
*/
onSubmit:
| ((name: string, setFieldError: (message: string) => void) => void)
| ((
name: string,
setFieldError: (message: string) => void,
) => Promise<void>);
};
/**
* A TextField and cancel/submit buttons.
*
* A common requirement is taking a single textual input from the user. This is
* a form suitable for that purpose. It contains a single MUI {@link TextField}
* and two accompanying buttons; one to submit, and one to cancel.
*
* Submission is handled as an async function, during which the input is
* disabled and a loading indicator is shown. Errors during submission are shown
* as the helper text associated with the text field.
*/
export const SingleInputForm: React.FC<SingleInputFormProps> = ({
inputType,
initialValue,
submitButtonTitle,
submitButtonColor,
onCancel,
onSubmit,
...rest
}) => {
const [showPassword, setShowPassword] = useState(false);
const handleToggleShowHidePassword = useCallback(
() => setShowPassword((show) => !show),
[],
);
const formik = useFormik({
initialValues: { value: initialValue ?? "" },
onSubmit: async (values, { setFieldError }) => {
const value = values.value;
const setValueFieldError = (message: string) =>
setFieldError("value", message);
if (!value) {
setValueFieldError(t("required"));
return;
}
try {
await onSubmit(value, setValueFieldError);
} catch (e) {
log.error(`Failed to submit input ${value}`, e);
setValueFieldError(t("generic_error"));
}
},
});
const submitButton = (
<LoadingButton
fullWidth
type="submit"
loading={formik.isSubmitting}
color={submitButtonColor ?? "primary"}
>
{submitButtonTitle}
</LoadingButton>
);
// Note: [Use space as default TextField helperText]
//
// For MUI text fields that use a conditional helperText, e.g. in case of
// errors, use an space as the default helperText in the other cases to
// avoid a layout shift when the helperText is conditionally shown.
return (
<form onSubmit={formik.handleSubmit}>
<TextField
name="value"
value={formik.values.value}
onChange={formik.handleChange}
type={showPassword ? "text" : (inputType ?? "text")}
fullWidth
margin="normal"
disabled={formik.isSubmitting}
error={!!formik.errors.value}
helperText={formik.errors.value ?? " "}
slotProps={{
input:
inputType == "password"
? {
endAdornment: (
<ShowHidePasswordInputAdornment
showPassword={showPassword}
onToggle={
handleToggleShowHidePassword
}
/>
),
}
: {},
}}
{...rest}
/>
{onCancel ? (
<Stack direction="row" sx={{ gap: "12px" }}>
<FocusVisibleButton
fullWidth
color="secondary"
onClick={onCancel}
>
{t("cancel")}
</FocusVisibleButton>
{submitButton}
</Stack>
) : (
submitButton
)}
</form>
);
};