Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [M3-8069] - scrollErrorIntoViewV2 - POC #10459

Merged
merged 8 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions docs/development-guide/05-fetching-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,104 @@ console.log(errorMap);
}
```

#### Scrolling to errors

For deep forms, we provide a utility that will scroll to the first error encountered within a defined container. We do this to improve error visibility, because the user can be unaware of an error that isn't in the viewport.
An error can be a notice (API error) or a Formik field error. In order to implement this often needed functionality, we must declare a form or form container via ref, then pass it to the `scrollErrorIntoViewV2` util (works both for class & functional components).

Note: the legacy `scrollErrorIntoView` is deprecated in favor of `scrollErrorIntoViewV2`.

Since Cloud Manager uses different ways of handling forms and validation, the `scrollErrorIntoViewV2` util should be implemented using the following patterns to ensure consistency.

##### Formik
```Typescript
import * as React from 'react';

import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2';

export const MyComponent = () => {
const formContainerRef = React.useRef<HTMLFormElement>(null);

const {
values,
// other handlers
} = useFormik({
initialValues: {},
onSubmit: mySubmitFormHandler,
validate: () => {
scrollErrorIntoViewV2(formRef);
},
validationSchema: myValidationSchema,
});

return (
<form onSubmit={handleSubmit} ref={formContainerRef}>
<Error />
{/* form fields */}
<button type="submit">Submit</button>
</form>
);
};
```

##### React Hook Forms
```Typescript
import * as React from 'react';

import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2';

export const MyComponent = () => {
const formContainerRef = React.useRef<HTMLFormElement>(null);

const methods = useForm<LinodeCreateFormValues>({
defaultValues,
mode: 'onBlur',
resolver: myResolvers,
// other methods
});

return (
<FormProvider {...methods}>
<form
onSubmit={methods.handleSubmit(onSubmit, () => scrollErrorIntoViewV2(formRef))}
ref={formContainerRef}
>
<Error />
{/* form fields */}
<button type="submit">Submit</button>
</form>
</>
);
};
```

##### Uncontrolled forms
```Typescript
import * as React from 'react';

import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2';

export const MyComponent = () => {
const formContainerRef = React.useRef<HTMLFormElement>(null);

const handleSubmit = () => {
try {
// form submission logic
} catch {
scrollErrorIntoViewV2(formContainerRef);
}
};

return (
<form onSubmit={handleSubmit} ref={formContainerRef}>
<Error />
{/* form fields */}
<button type="submit">Submit</button>
</form>
);
};
```

### Toast / Event Message Punctuation
**Best practice:**
- If a message is a sentence or a sentence fragment with a subject and a verb, add punctuation. Otherwise, leave punctuation off.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import {
ipFieldPlaceholder,
validateIPs,
} from 'src/utilities/ipUtils';
import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView';
import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2';

import type { PlanSelectionType } from 'src/features/components/PlansPanel/types';

Expand Down Expand Up @@ -213,6 +213,7 @@ const DatabaseCreate = () => {
isLoading: typesLoading,
} = useDatabaseTypesQuery();

const formRef = React.useRef<HTMLFormElement>(null);
const { mutateAsync: createDatabase } = useCreateDatabaseMutation();

const [nodePricing, setNodePricing] = React.useState<NodePricing>();
Expand Down Expand Up @@ -316,7 +317,10 @@ const DatabaseCreate = () => {
type: '',
},
onSubmit: submitForm,
validate: handleIPValidation,
validate: () => {
handleIPValidation();
scrollErrorIntoViewV2(formRef);
},
validateOnChange: false,
validationSchema: createDatabaseSchema,
});
Expand Down Expand Up @@ -351,12 +355,6 @@ const DatabaseCreate = () => {
});
}, [dbtypes, selectedEngine]);

React.useEffect(() => {
if (errors || createError) {
scrollErrorIntoView();
}
}, [errors, createError]);

const labelToolTip = (
<div className={classes.labelToolTipCtn}>
<strong>Label must:</strong>
Expand Down Expand Up @@ -444,7 +442,7 @@ const DatabaseCreate = () => {
}

return (
<form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit} ref={formRef}>
<LandingHeader
breadcrumbProps={{
crumbOverrides: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
restoreBackup,
} from '@linode/api-v4/lib/linodes';
import { Tag } from '@linode/api-v4/lib/tags/types';
import { CreateLinodeSchema } from '@linode/validation/lib/linodes.schema';
import Grid from '@mui/material/Unstable_Grid2';
import cloneDeep from 'lodash.clonedeep';
import * as React from 'react';
Expand Down Expand Up @@ -76,6 +77,7 @@ import { getMonthlyBackupsPrice } from 'src/utilities/pricing/backups';
import { UNKNOWN_PRICE } from 'src/utilities/pricing/constants';
import { renderMonthlyPriceToCorrectDecimalPlace } from 'src/utilities/pricing/dynamicPricing';
import { getQueryParamsFromQueryString } from 'src/utilities/queryParams';
import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2';

import { SelectFirewallPanel } from '../../../components/SelectFirewallPanel/SelectFirewallPanel';
import { AddonsPanel } from './AddonsPanel';
Expand Down Expand Up @@ -194,6 +196,7 @@ type CombinedProps = AllFormStateAndHandlers &
WithTypesProps;

interface State {
hasError: boolean;
numberOfNodes: number;
planKey: string;
selectedTab: number;
Expand Down Expand Up @@ -238,6 +241,7 @@ export class LinodeCreate extends React.PureComponent<
}

this.state = {
hasError: false,
numberOfNodes: 0,
planKey: v4(),
selectedTab: preSelectedTab !== -1 ? preSelectedTab : 0,
Expand Down Expand Up @@ -487,7 +491,7 @@ export class LinodeCreate extends React.PureComponent<
);

return (
<StyledForm>
<StyledForm ref={this.createLinodeFormRef}>
<Grid className="py0">
{hasErrorFor.none && !!showGeneralError && (
<Notice spacingTop={8} text={hasErrorFor.none} variant="error" />
Expand Down Expand Up @@ -855,6 +859,16 @@ export class LinodeCreate extends React.PureComponent<
const { selectedTab } = this.state;
const selectedTabName = this.tabs[selectedTab].title as LinodeCreateType;

try {
CreateLinodeSchema.validateSync(payload, {
abortEarly: true,
});
this.setState({ hasError: false });
} catch (e) {
this.setState({ hasError: true }, () => {
scrollErrorIntoViewV2(this.createLinodeFormRef);
});
}
this.props.handleSubmitForm(payload, this.props.selectedLinodeID);
sendLinodeCreateFormSubmitEvent(
'Create Linode',
Expand All @@ -863,6 +877,8 @@ export class LinodeCreate extends React.PureComponent<
);
};

createLinodeFormRef = React.createRef<HTMLFormElement>();

filterTypes = () => {
const { createType, typesData } = this.props;
const { selectedTab } = this.state;
Expand Down
Loading
Loading