fixes bug with autocompleted input fields
This commit is contained in:
@@ -150,6 +150,7 @@ function fieldReducer(state, action) {
|
||||
ignoreWhileFlattening: action.ignoreWhileFlattening,
|
||||
initialValue: action.initialValue,
|
||||
stringify: action.stringify,
|
||||
validate: action.validate,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -14,6 +14,7 @@ import initContextState from './initContextState';
|
||||
import reduceFieldsToValues from './reduceFieldsToValues';
|
||||
import getSiblingDataFunc from './getSiblingData';
|
||||
import getDataByPathFunc from './getDataByPath';
|
||||
import wait from '../../../../utilities/wait';
|
||||
|
||||
import { SubmittedContext, ProcessingContext, ModifiedContext, FormContext, FieldContext } from './context';
|
||||
|
||||
@@ -35,6 +36,7 @@ const Form = (props) => {
|
||||
disableSuccessStatus,
|
||||
initialState,
|
||||
disableScrollOnSuccess,
|
||||
waitForAutocomplete,
|
||||
} = props;
|
||||
|
||||
const history = useHistory();
|
||||
@@ -53,21 +55,50 @@ const Form = (props) => {
|
||||
const [fields, dispatchFields] = useReducer(fieldReducer, {});
|
||||
contextRef.current.fields = fields;
|
||||
|
||||
const submit = useCallback((e) => {
|
||||
const validateForm = useCallback(async () => {
|
||||
const validatedFieldState = {};
|
||||
let isValid = true;
|
||||
|
||||
const validationPromises = Object.entries(contextRef.current.fields).map(async ([path, field]) => {
|
||||
const validatedField = { ...field };
|
||||
|
||||
validatedField.valid = typeof field.validate === 'function' ? await field.validate(field.value) : true;
|
||||
|
||||
if (typeof validatedField.valid === 'string') {
|
||||
validatedField.errorMessage = validatedField.valid;
|
||||
validatedField.valid = false;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
validatedFieldState[path] = validatedField;
|
||||
});
|
||||
|
||||
await Promise.all(validationPromises);
|
||||
|
||||
dispatchFields({ type: 'REPLACE_STATE', state: validatedFieldState });
|
||||
|
||||
return isValid;
|
||||
}, [contextRef]);
|
||||
|
||||
const submit = useCallback(async (e) => {
|
||||
if (disabled) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
setSubmitted(true);
|
||||
e.preventDefault();
|
||||
|
||||
const isValid = contextRef.current.validateForm();
|
||||
setProcessing(true);
|
||||
|
||||
if (waitForAutocomplete) await wait(100);
|
||||
|
||||
const isValid = await contextRef.current.validateForm();
|
||||
|
||||
setSubmitted(true);
|
||||
|
||||
// If not valid, prevent submission
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
|
||||
addStatus({
|
||||
message: 'Please correct the fields below.',
|
||||
type: 'error',
|
||||
@@ -85,12 +116,9 @@ const Form = (props) => {
|
||||
|
||||
// If submit handler comes through via props, run that
|
||||
if (onSubmit) {
|
||||
e.preventDefault();
|
||||
return onSubmit(fields);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
if (!disableScrollOnSuccess) {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
@@ -99,103 +127,104 @@ const Form = (props) => {
|
||||
}
|
||||
|
||||
const formData = contextRef.current.createFormData();
|
||||
setProcessing(true);
|
||||
|
||||
// Make the API call from the action
|
||||
return requests[method.toLowerCase()](action, {
|
||||
body: formData,
|
||||
}).then((res) => {
|
||||
setModified(false);
|
||||
try {
|
||||
const res = await requests[method.toLowerCase()](action, {
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (typeof handleResponse === 'function') return handleResponse(res);
|
||||
|
||||
return res.json().then((json) => {
|
||||
setProcessing(false);
|
||||
clearStatus();
|
||||
const json = await res.json();
|
||||
|
||||
if (res.status < 400) {
|
||||
setSubmitted(false);
|
||||
setProcessing(false);
|
||||
clearStatus();
|
||||
|
||||
if (typeof onSuccess === 'function') onSuccess(json);
|
||||
if (res.status < 400) {
|
||||
setSubmitted(false);
|
||||
|
||||
if (redirect) {
|
||||
const destination = {
|
||||
pathname: redirect,
|
||||
if (typeof onSuccess === 'function') onSuccess(json);
|
||||
|
||||
if (redirect) {
|
||||
const destination = {
|
||||
pathname: redirect,
|
||||
};
|
||||
|
||||
if (json.message && !disableSuccessStatus) {
|
||||
destination.state = {
|
||||
status: [
|
||||
{
|
||||
message: json.message,
|
||||
type: 'success',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (json.message && !disableSuccessStatus) {
|
||||
destination.state = {
|
||||
status: [
|
||||
{
|
||||
message: json.message,
|
||||
type: 'success',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
history.push(destination);
|
||||
} else if (!disableSuccessStatus) {
|
||||
replaceStatus([{
|
||||
message: json.message,
|
||||
type: 'success',
|
||||
disappear: 3000,
|
||||
}]);
|
||||
}
|
||||
} else {
|
||||
contextRef.current = { ...contextRef.current }; // triggers rerender of all components that subscribe to form
|
||||
|
||||
if (json.message) {
|
||||
addStatus({
|
||||
message: json.message,
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
if (Array.isArray(json.errors)) {
|
||||
const [fieldErrors, nonFieldErrors] = json.errors.reduce(([fieldErrs, nonFieldErrs], err) => (err.field && err.message ? [[...fieldErrs, err], nonFieldErrs] : [fieldErrs, [...nonFieldErrs, err]]), [[], []]);
|
||||
|
||||
fieldErrors.forEach((err) => {
|
||||
dispatchFields({
|
||||
...(contextRef.current?.fields?.[err.field] || {}),
|
||||
valid: false,
|
||||
errorMessage: err.message,
|
||||
path: err.field,
|
||||
});
|
||||
});
|
||||
|
||||
nonFieldErrors.forEach((err) => {
|
||||
addStatus({
|
||||
message: err.message || 'An unknown error occurred.',
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
if (fieldErrors.length > 0 && nonFieldErrors.length === 0) {
|
||||
addStatus({
|
||||
message: 'Please correct the fields below.',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
history.push(destination);
|
||||
} else if (!disableSuccessStatus) {
|
||||
replaceStatus([{
|
||||
message: json.message,
|
||||
type: 'success',
|
||||
disappear: 3000,
|
||||
}]);
|
||||
}
|
||||
} else {
|
||||
contextRef.current = { ...contextRef.current }; // triggers rerender of all components that subscribe to form
|
||||
|
||||
if (json.message) {
|
||||
addStatus({
|
||||
message: 'An unknown error occurred.',
|
||||
message: json.message,
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
return json;
|
||||
});
|
||||
}).catch((err) => {
|
||||
addStatus({
|
||||
if (Array.isArray(json.errors)) {
|
||||
const [fieldErrors, nonFieldErrors] = json.errors.reduce(([fieldErrs, nonFieldErrs], err) => (err.field && err.message ? [[...fieldErrs, err], nonFieldErrs] : [fieldErrs, [...nonFieldErrs, err]]), [[], []]);
|
||||
|
||||
fieldErrors.forEach((err) => {
|
||||
dispatchFields({
|
||||
...(contextRef.current?.fields?.[err.field] || {}),
|
||||
valid: false,
|
||||
errorMessage: err.message,
|
||||
path: err.field,
|
||||
});
|
||||
});
|
||||
|
||||
nonFieldErrors.forEach((err) => {
|
||||
addStatus({
|
||||
message: err.message || 'An unknown error occurred.',
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
if (fieldErrors.length > 0 && nonFieldErrors.length === 0) {
|
||||
addStatus({
|
||||
message: 'Please correct the fields below.',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
addStatus({
|
||||
message: 'An unknown error occurred.',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
return json;
|
||||
} catch (err) {
|
||||
setProcessing(false);
|
||||
|
||||
return addStatus({
|
||||
message: err,
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [
|
||||
action,
|
||||
addStatus,
|
||||
@@ -211,6 +240,7 @@ const Form = (props) => {
|
||||
redirect,
|
||||
replaceStatus,
|
||||
disableScrollOnSuccess,
|
||||
waitForAutocomplete,
|
||||
]);
|
||||
|
||||
|
||||
@@ -224,8 +254,6 @@ const Form = (props) => {
|
||||
|
||||
const getUnflattenedValues = useCallback(() => reduceFieldsToValues(contextRef.current.fields), [contextRef]);
|
||||
|
||||
const validateForm = useCallback(() => !Object.values(contextRef.current.fields).some((field) => field.valid === false), [contextRef]);
|
||||
|
||||
const createFormData = useCallback(() => {
|
||||
const data = reduceFieldsToValues(contextRef.current.fields);
|
||||
|
||||
@@ -306,6 +334,7 @@ Form.defaultProps = {
|
||||
disabled: false,
|
||||
initialState: {},
|
||||
disableScrollOnSuccess: false,
|
||||
waitForAutocomplete: false,
|
||||
};
|
||||
|
||||
Form.propTypes = {
|
||||
@@ -324,6 +353,7 @@ Form.propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
initialState: PropTypes.shape({}),
|
||||
disableScrollOnSuccess: PropTypes.bool,
|
||||
waitForAutocomplete: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Form;
|
||||
|
||||
@@ -56,6 +56,7 @@ const useFieldType = (options) => {
|
||||
fieldToDispatch.disableFormData = disableFormData;
|
||||
fieldToDispatch.ignoreWhileFlattening = ignoreWhileFlattening;
|
||||
fieldToDispatch.initialValue = initialValue;
|
||||
fieldToDispatch.validate = validate;
|
||||
|
||||
dispatchFields(fieldToDispatch);
|
||||
}, [path, dispatchFields, validate, disableFormData, ignoreWhileFlattening, initialValue, stringify]);
|
||||
|
||||
@@ -140,7 +140,7 @@ const AuthenticationProvider = ({ children }) => {
|
||||
if (remainingTime > 0) {
|
||||
forceLogOut = setTimeout(() => {
|
||||
setUser(null);
|
||||
history.push(`${admin}/logout`);
|
||||
history.push(`${admin}/logout-inactivity`);
|
||||
closeAllModals();
|
||||
}, remainingTime * 1000);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ const Login = () => {
|
||||
</div>
|
||||
<Form
|
||||
disableSuccessStatus
|
||||
waitForAutocomplete
|
||||
onSuccess={onSuccess}
|
||||
method="POST"
|
||||
action={`${serverURL}${api}/${userSlug}/login`}
|
||||
|
||||
@@ -114,6 +114,16 @@ $focus-box-shadow: 0 0 0 $style-stroke-width-m $color-green;
|
||||
backdrop-filter: saturate(180%) blur(5px);
|
||||
}
|
||||
|
||||
@keyframes onAutoFillStart {
|
||||
from {/**/}
|
||||
to {/**/}
|
||||
}
|
||||
|
||||
@keyframes onAutoFillCancel {
|
||||
from {/**/}
|
||||
to {/**/}
|
||||
}
|
||||
|
||||
@mixin formInput () {
|
||||
@include inputShadow;
|
||||
font-family: $font-body;
|
||||
@@ -159,6 +169,17 @@ $focus-box-shadow: 0 0 0 $style-stroke-width-m $color-green;
|
||||
}
|
||||
}
|
||||
|
||||
// Thanks, Klarna:
|
||||
|
||||
&:-webkit-autofill {
|
||||
animation-name: onAutoFillStart;
|
||||
transition: background-color 50000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
&:not(:-webkit-autofill) {
|
||||
animation-name: onAutoFillCancel;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
margin-bottom: base(.5);
|
||||
}
|
||||
|
||||
7
src/utilities/wait.js
Normal file
7
src/utilities/wait.js
Normal file
@@ -0,0 +1,7 @@
|
||||
async function wait(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = wait;
|
||||
Reference in New Issue
Block a user