diff --git a/src/.eslintrc.json b/src/.eslintrc.json index 4f29360def..1f1f524973 100644 --- a/src/.eslintrc.json +++ b/src/.eslintrc.json @@ -59,7 +59,7 @@ "import/no-unresolved": ["off"], "@typescript-eslint/no-unused-vars": [ "error", - { "ignoreRestSiblings": true } + { "ignoreRestSiblings": true, "argsIgnorePattern": "^_" } ], "@typescript-eslint/consistent-type-imports": ["warn"], "react-hooks/exhaustive-deps": ["error"], diff --git a/src/altinn-app-frontend/package.json b/src/altinn-app-frontend/package.json index 5fb0a95a9e..6d376a181a 100644 --- a/src/altinn-app-frontend/package.json +++ b/src/altinn-app-frontend/package.json @@ -18,7 +18,7 @@ "author": "Altinn", "license": "3-Clause BSD", "dependencies": { - "@altinn/altinn-design-system": "0.1.4", + "@altinn/altinn-design-system": "^0.2.0", "@babel/polyfill": "^7.12.1", "@date-io/moment": "1.3.13", "@material-ui/core": "^4.12.4", diff --git a/src/altinn-app-frontend/src/components/GenericComponent.tsx b/src/altinn-app-frontend/src/components/GenericComponent.tsx index fe40329632..242e44aa49 100644 --- a/src/altinn-app-frontend/src/components/GenericComponent.tsx +++ b/src/altinn-app-frontend/src/components/GenericComponent.tsx @@ -101,6 +101,7 @@ export function GenericComponent( const { id, ...passThroughProps } = props; const dispatch = useAppDispatch(); const classes = useStyles(props); + const gridRef = React.useRef(); const GetHiddenSelector = makeGetHidden(); const GetFocusSelector = makeGetFocus(); const [hasValidationMessages, setHasValidationMessages] = @@ -154,6 +155,20 @@ export function GenericComponent( ); }, [componentValidations]); + React.useLayoutEffect(() => { + if (!hidden && shouldFocus && gridRef.current) { + gridRef.current.scrollIntoView(); + + const maybeInput = gridRef.current.querySelector( + 'input,textarea,select', + ) as HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement; + if (maybeInput) { + maybeInput.focus(); + } + dispatch(FormLayoutActions.updateFocus({ focusComponentId: null })); + } + }, [shouldFocus, hidden, dispatch]); + if (hidden) { return null; } @@ -199,15 +214,6 @@ export function GenericComponent( ); }; - const handleFocusUpdate = (componentId: string, step?: number) => { - dispatch( - FormLayoutActions.updateFocus({ - currentComponentId: componentId, - step: step || 0, - }), - ); - }; - const getValidationsForInternalHandling = () => { if ( props.type === 'AddressComponent' || @@ -288,7 +294,6 @@ export function GenericComponent( const componentProps = { handleDataChange, - handleFocusUpdate, getTextResource: getTextResourceWrapper, getTextResourceAsString, formData, @@ -332,6 +337,7 @@ export function GenericComponent( return ( {return;} +import { nb } from '../../shared/resources/language/texts/nb'; +const dummyFunc = () => { + return; +}; const legend = () => { return ( @@ -15,13 +17,12 @@ const legend = () => { helpTextProps={{}} /> ); -} +}; const props = { id: 'simpleCheckbox', formData: '', handleDataChange: dummyFunc, - handleFocusUpdate: dummyFunc, isValid: true, validationMessages: {}, options: [ @@ -44,9 +45,5 @@ const props = { legend, }; - - +; ``` - diff --git a/src/altinn-app-frontend/src/components/base/CheckboxesContainerComponent.test.tsx b/src/altinn-app-frontend/src/components/base/CheckboxesContainerComponent.test.tsx index e923497e91..9a17ffa136 100644 --- a/src/altinn-app-frontend/src/components/base/CheckboxesContainerComponent.test.tsx +++ b/src/altinn-app-frontend/src/components/base/CheckboxesContainerComponent.test.tsx @@ -42,7 +42,6 @@ const render = ( validationMessages: {}, legend: 'legend', handleDataChange: jest.fn(), - handleFocusUpdate: jest.fn(), getTextResource: (value) => value, getTextResourceAsString: (value) => value, ...({} as IComponentProps), diff --git a/src/altinn-app-frontend/src/components/base/CheckboxesContainerComponent.tsx b/src/altinn-app-frontend/src/components/base/CheckboxesContainerComponent.tsx index aadaf63447..389403c809 100644 --- a/src/altinn-app-frontend/src/components/base/CheckboxesContainerComponent.tsx +++ b/src/altinn-app-frontend/src/components/base/CheckboxesContainerComponent.tsx @@ -85,7 +85,6 @@ export const CheckboxContainerComponent = ({ formData, preselectedOptionIndex, handleDataChange, - handleFocusUpdate, layout, legend, getTextResourceAsString, @@ -146,7 +145,6 @@ export const CheckboxContainerComponent = ({ } else { handleDataChange(selected.concat(clickedItem).join(',')); } - handleFocusUpdate(id); }; const handleBlur = () => { diff --git a/src/altinn-app-frontend/src/components/base/RadioButtons/ControlledRadioGroup.tsx b/src/altinn-app-frontend/src/components/base/RadioButtons/ControlledRadioGroup.tsx index 06c4ede00d..3bed2e8b2d 100644 --- a/src/altinn-app-frontend/src/components/base/RadioButtons/ControlledRadioGroup.tsx +++ b/src/altinn-app-frontend/src/components/base/RadioButtons/ControlledRadioGroup.tsx @@ -28,7 +28,6 @@ export const ControlledRadioGroup = ({ id, layout, legend, - shouldFocus, getTextResource, validationMessages, fetchingOptions, @@ -66,11 +65,7 @@ export const ControlledRadioGroup = ({ {calculatedOptions.map((option: any, index: number) => ( - } + control={} label={getTextResource(option.label)} value={option.value} classes={{ root: cn(classes.margin) }} diff --git a/src/altinn-app-frontend/src/components/base/RadioButtons/RadioButtonsContainerComponent.test.tsx b/src/altinn-app-frontend/src/components/base/RadioButtons/RadioButtonsContainerComponent.test.tsx index ae68fbbd82..cfb4670a60 100644 --- a/src/altinn-app-frontend/src/components/base/RadioButtons/RadioButtonsContainerComponent.test.tsx +++ b/src/altinn-app-frontend/src/components/base/RadioButtons/RadioButtonsContainerComponent.test.tsx @@ -41,7 +41,6 @@ const render = ( preselectedOptionIndex: undefined, legend: 'legend', handleDataChange: jest.fn(), - handleFocusUpdate: jest.fn(), getTextResource: (value) => value, ...({} as IComponentProps), ...props, diff --git a/src/altinn-app-frontend/src/components/base/RadioButtons/radioButtonsUtils.ts b/src/altinn-app-frontend/src/components/base/RadioButtons/radioButtonsUtils.ts index 64f059a81e..455d0d85c7 100644 --- a/src/altinn-app-frontend/src/components/base/RadioButtons/radioButtonsUtils.ts +++ b/src/altinn-app-frontend/src/components/base/RadioButtons/radioButtonsUtils.ts @@ -54,10 +54,8 @@ export const useRadioStyles = makeStyles((theme) => ({ })); export const useRadioButtons = ({ - id, optionsId, options, - handleFocusUpdate, handleDataChange, preselectedOptionIndex, formData, @@ -103,7 +101,6 @@ export const useRadioButtons = ({ }, [handleDataChange, optionsHasChanged, formData]); const handleChange = (event: React.ChangeEvent) => { - handleFocusUpdate(id); handleDataChange(event.target.value); }; diff --git a/src/altinn-app-frontend/src/components/custom/CustomWebComponent.test.tsx b/src/altinn-app-frontend/src/components/custom/CustomWebComponent.test.tsx index d5f520ed22..ec24b98c28 100644 --- a/src/altinn-app-frontend/src/components/custom/CustomWebComponent.test.tsx +++ b/src/altinn-app-frontend/src/components/custom/CustomWebComponent.test.tsx @@ -39,7 +39,6 @@ describe('CustomWebComponent', () => { title: 'Title', }, handleDataChange: (value: string) => value, - handleFocusUpdate: jest.fn(), getTextResource: (key: string) => { return key; }, diff --git a/src/altinn-app-frontend/src/components/index.ts b/src/altinn-app-frontend/src/components/index.ts index 5986eeeb4a..ea2d9952e6 100644 --- a/src/altinn-app-frontend/src/components/index.ts +++ b/src/altinn-app-frontend/src/components/index.ts @@ -63,7 +63,6 @@ export interface IComponentProps extends IGenericComponentProps { skipValidation?: boolean, checkIfRequired?: boolean, ) => void; - handleFocusUpdate: (componentId: string, step?: number) => void; getTextResource: (key: string) => React.ReactNode; getTextResourceAsString: (key: string) => string; language: ILanguage; diff --git a/src/altinn-app-frontend/src/components/message/ErrorReport.test.tsx b/src/altinn-app-frontend/src/components/message/ErrorReport.test.tsx index 235a7e6958..0d21740c63 100644 --- a/src/altinn-app-frontend/src/components/message/ErrorReport.test.tsx +++ b/src/altinn-app-frontend/src/components/message/ErrorReport.test.tsx @@ -11,11 +11,6 @@ import type { IValidations } from 'src/types'; import { getParsedLanguageFromText } from 'altinn-shared/utils'; describe('ErrorReport', () => { - const genericErrorText = - getInitialStateMock().language.language.form_filler[ - 'error_report_description' - ]; - const render = (validations: Partial) => { const mockValidationState: IValidationState = { validations: { @@ -29,27 +24,17 @@ describe('ErrorReport', () => { formValidations: mockValidationState, }); - return renderWithProviders(, { + return renderWithProviders(, { preloadedState: initialState, }); }; - it('should render generic error message by default', () => { - const validations = { - page1: { - someComponent: { - simpleBinding: { - errors: [getParsedLanguageFromText('some error')], - }, - }, - }, - }; - render(validations); - - expect(screen.getByText(genericErrorText)).toBeInTheDocument(); + it('should not render when there are no errors', () => { + render({}); + expect(screen.queryByTestId('ErrorReport')).not.toBeInTheDocument(); }); - it('should list unmapped errors if present and hide generic error message', () => { + it('should list unmapped errors as unclickable', () => { const validations = { unmapped: { // unmapped layout @@ -64,8 +49,31 @@ describe('ErrorReport', () => { }; render(validations); + expect(screen.getByTestId('ErrorReport')).toBeInTheDocument(); + + // Unmapped errors should not be clickable + const errorNode = screen.getByText('some unmapped error'); + expect(errorNode).toBeInTheDocument(); + expect(errorNode.parentElement.tagName).toEqual('LI'); + }); + + it('should list mapped error as clickable', () => { + const validations = { + page1: { + someComponent: { + simpleBinding: { + errors: [getParsedLanguageFromText('some mapped error')], + }, + }, + }, + }; + + render(validations); + expect(screen.getByTestId('ErrorReport')).toBeInTheDocument(); - expect(screen.queryByText(genericErrorText)).not.toBeInTheDocument(); - expect(screen.getByText('some unmapped error')).toBeInTheDocument(); + const errorNode = screen.getByText('some mapped error'); + expect(errorNode).toBeInTheDocument(); + expect(errorNode.parentElement.parentElement.tagName).toEqual('LI'); + expect(errorNode.parentElement.tagName).toEqual('BUTTON'); }); }); diff --git a/src/altinn-app-frontend/src/components/message/ErrorReport.tsx b/src/altinn-app-frontend/src/components/message/ErrorReport.tsx index d773484021..09d60f012a 100644 --- a/src/altinn-app-frontend/src/components/message/ErrorReport.tsx +++ b/src/altinn-app-frontend/src/components/message/ErrorReport.tsx @@ -1,128 +1,141 @@ import * as React from 'react'; -import { useAppSelector } from 'src/common/hooks'; -import { getUnmappedErrors } from 'src/utils/validation'; -import type { IValidations } from 'src/types'; +import { Panel, PanelVariant } from '@altinn/altinn-design-system'; +import { Grid, makeStyles } from '@material-ui/core'; + +import { useAppDispatch, useAppSelector } from 'src/common/hooks'; +import { FullWidthWrapper } from 'src/features/form/components/FullWidthWrapper'; +import { renderLayoutComponent } from 'src/features/form/containers/Form'; +import { FormLayoutActions } from 'src/features/form/layout/formLayoutSlice'; +import { getMappedErrors, getUnmappedErrors } from 'src/utils/validation'; +import type { ILayout } from 'src/features/form/layout'; +import type { FlatError } from 'src/utils/validation'; import { getLanguageFromKey } from 'altinn-shared/utils'; -const ErrorReport = () => { - const validations = useAppSelector( - (state) => state.formValidations.validations, +export interface IErrorReportProps { + components: ILayout; +} + +const ArrowForwardIcon = ` + +`; + +const useStyles = makeStyles((theme) => ({ + errorList: { + listStylePosition: 'inside', + listStyleImage: `url("data:image/svg+xml,${encodeURIComponent( + ArrowForwardIcon, + )}")`, + '& > li': { + marginBottom: theme.spacing(1), + }, + }, + buttonAsInvisibleLink: { + backgroundColor: 'transparent', + border: 'none', + cursor: 'pointer', + textDecoration: 'none', + display: 'inline', + margin: 0, + padding: 0, + }, +})); + +const ErrorReport = ({ components }: IErrorReportProps) => { + const classes = useStyles(); + const dispatch = useAppDispatch(); + const currentView = useAppSelector( + (state) => state.formLayout.uiConfig.currentView, ); - const unmappedErrors = getUnmappedErrors(validations); - const hasUnmappedErrors = unmappedErrors.length > 0; + const [errorsMapped, errorsUnmapped] = useAppSelector((state) => [ + getMappedErrors(state.formValidations.validations), + getUnmappedErrors(state.formValidations.validations), + ]); const language = useAppSelector((state) => state.language.language); - const formHasErrors = useAppSelector((state) => - getFormHasErrors(state.formValidations.validations), - ); - const hasSubmitted = useAppSelector((state) => state.formData.hasSubmitted); - const errorRef = React.useRef(null); - - React.useEffect(() => { - if (hasSubmitted) { - errorRef?.current?.focus(); - } - }, [hasSubmitted, unmappedErrors]); + const hasErrors = errorsUnmapped.length > 0 || errorsMapped.length > 0; - if (!formHasErrors) { + if (!hasErrors) { return null; } + const handleErrorClick = + (error: FlatError) => (ev: React.KeyboardEvent | React.MouseEvent) => { + if ( + ev.type === 'keydown' && + (ev as React.KeyboardEvent).key !== 'Enter' + ) { + return; + } + ev.preventDefault(); + if (currentView === error.layout) { + dispatch( + FormLayoutActions.updateFocus({ + focusComponentId: error.componentId, + }), + ); + } else { + dispatch( + FormLayoutActions.updateCurrentView({ + newView: error.layout, + runValidations: null, + returnToView: currentView, + focusComponentId: error.componentId, + }), + ); + } + }; + return ( -
-
-
-
-
+ + + -
-
-