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

WCAG: Validation errors #2440

Merged
merged 10 commits into from
Sep 19, 2024
48 changes: 0 additions & 48 deletions src/components/form/SoftValidations.test.tsx

This file was deleted.

36 changes: 0 additions & 36 deletions src/components/form/SoftValidations.tsx

This file was deleted.

110 changes: 49 additions & 61 deletions src/features/validation/ComponentValidations.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import React from 'react';

import { ErrorMessage } from '@digdir/designsystemet-react';
import { Alert as AlertDesignSystem, ErrorMessage } from '@digdir/designsystemet-react';

import { Lang } from 'src/features/language/Lang';
import { useLanguage } from 'src/features/language/useLanguage';
import { useUnifiedValidationsForNode } from 'src/features/validation/selectors/unifiedValidationsForNode';
import { validationsOfSeverity } from 'src/features/validation/utils';
import { AlertBaseComponent } from 'src/layout/Alert/AlertBaseComponent';
import { useCurrentNode } from 'src/layout/FormComponentContext';
import { useNodeItem } from 'src/utils/layout/useNodeItem';
import { useGetUniqueKeyFromObject } from 'src/utils/useGetKeyFromObject';
Expand All @@ -32,16 +30,13 @@ export function ComponentValidations({ validations, node: _node }: Props) {
const inputMaxLength = useNodeItem(node, (i) =>
i.type === 'Input' || i.type === 'TextArea' ? i.maxLength : undefined,
);
if (!validations || validations.length === 0 || !node) {
return null;
}

// If maxLength is set in both schema and component, don't display the schema error message here.
// TODO: This should preferably be implemented in the Input component, via ValidationFilter, but that causes
// cypress tests in `components.ts` to fail.
// @see https://github.com/Altinn/app-frontend-react/issues/1263
const filteredValidations = inputMaxLength
? validations.filter(
? validations?.filter(
(validation) =>
!(
validation.message.key === 'validation_errors.maxLength' &&
Expand All @@ -56,34 +51,41 @@ export function ComponentValidations({ validations, node: _node }: Props) {
const success = validationsOfSeverity(filteredValidations, 'success');

return (
<div data-validation={node.id}>
{errors.length > 0 && (
<ErrorValidations
validations={errors}
node={node}
/>
)}
{warnings.length > 0 && (
<SoftValidations
validations={warnings}
variant='warning'
node={node}
/>
)}
{info.length > 0 && (
<SoftValidations
validations={info}
variant='info'
node={node}
/>
)}
{success.length > 0 && (
<SoftValidations
validations={success}
variant='success'
node={node}
/>
)}
<div
aria-live='assertive'
style={{ display: 'contents' }}
>
{node && validations?.length ? (
<div data-validation={node.id}>
{errors.length > 0 && (
<ErrorValidations
validations={errors}
node={node}
/>
)}
{warnings.length > 0 && (
<SoftValidations
validations={warnings}
severity='warning'
node={node}
/>
)}
{info.length > 0 && (
<SoftValidations
validations={info}
severity='info'
node={node}
/>
)}
{success.length > 0 && (
<SoftValidations
validations={success}
severity='success'
node={node}
/>
)}
</div>
) : null}
</div>
);
}
Expand All @@ -92,13 +94,10 @@ function ErrorValidations({ validations, node }: { validations: BaseValidation<'
const getUniqueKeyFromObject = useGetUniqueKeyFromObject();

return (
<ol style={{ padding: 0, margin: 0, listStyleType: 'none' }}>
<ul style={{ padding: 0, margin: 0, listStyleType: 'none' }}>
{validations.map((validation) => (
<li key={getUniqueKeyFromObject(validation)}>
<ErrorMessage
role='alert'
size='small'
>
<ErrorMessage size='small'>
<Lang
id={validation.message.key}
params={validation.message.params}
Expand All @@ -107,50 +106,39 @@ function ErrorValidations({ validations, node }: { validations: BaseValidation<'
</ErrorMessage>
</li>
))}
</ol>
</ul>
);
}

function SoftValidations({
validations,
variant,
severity,
node,
}: {
validations: BaseValidation<'warning' | 'info' | 'success'>[];
variant: AlertSeverity;
severity: AlertSeverity;
node: LayoutNode;
}) {
const getUniqueKeyFromObject = useGetUniqueKeyFromObject();
const { langAsString } = useLanguage();

/**
* Rendering the error messages as an ordered
* list with each error message as a list item.
*/
const ariaLabel = validations.map((v) => langAsString(v.message.key, v.message.params)).join();

return (
<div style={{ paddingTop: 'var(--fds-spacing-2)' }}>
<AlertBaseComponent
severity={variant}
useAsAlert={true}
ariaLabel={ariaLabel}
<AlertDesignSystem
style={{ breakInside: 'avoid' }}
severity={severity}
>
<ol style={{ paddingLeft: 0, listStyleType: 'none' }}>
<ul style={{ paddingLeft: 0, listStyleType: 'none' }}>
{validations.map((validation) => (
<li
role='alert'
key={getUniqueKeyFromObject(validation)}
>
<li key={getUniqueKeyFromObject(validation)}>
<Lang
id={validation.message.key}
params={validation.message.params}
node={node}
/>
</li>
))}
</ol>
</AlertBaseComponent>
</ul>
</AlertDesignSystem>
</div>
);
}
4 changes: 2 additions & 2 deletions src/layout/Address/AddressComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,12 @@ describe('AddressComponent', () => {
},
});

expect(screen.queryByRole('alert')).not.toBeInTheDocument();
expect(screen.queryByText(/postnummer er ugyldig/i)).not.toBeInTheDocument();

await userEvent.type(screen.getByRole('textbox', { name: 'Postnr *' }), '1');
await userEvent.tab();

expect(screen.getByRole('alert')).toHaveTextContent('Postnummer er ugyldig. Et postnummer består kun av 4 siffer.');
expect(screen.getByText(/postnummer er ugyldig/i)).toBeInTheDocument();
});

it('should update postplace on mount', async () => {
Expand Down
2 changes: 1 addition & 1 deletion src/layout/Alert/AlertBaseComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const AlertBaseComponent = ({ title, children, useAsAlert, severity, aria
aria-live={useAsAlert ? calculateAriaLive(severity) : undefined}
aria-label={useAsAlert ? (ariaLabel ?? title) : undefined}
>
<span className={styles.title}>{title}</span>
{title && <span className={styles.title}>{title}</span>}
<div className={styles.body}>{children}</div>
</AlertDesignSystem>
);
6 changes: 3 additions & 3 deletions src/layout/Likert/LikertComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ describe('RepeatingGroupsLikertContainer', () => {
await waitFor(() => {
expect(screen.getByRole('table')).toBeInTheDocument();
});
expect(screen.getByRole('alert')).toHaveTextContent('Feltet er påkrevd');
expect(screen.getByText(/feltet er påkrevd/i)).toBeInTheDocument();
});

it('should render 2 validations', async () => {
Expand All @@ -239,7 +239,7 @@ describe('RepeatingGroupsLikertContainer', () => {
await waitFor(() => {
expect(screen.getByRole('table')).toBeInTheDocument();
});
expect(screen.getAllByRole('alert')).toHaveLength(2);
expect(screen.getAllByText(/feltet er påkrevd/i)).toHaveLength(2);
});

it('should display title and description', async () => {
Expand Down Expand Up @@ -354,7 +354,7 @@ describe('RepeatingGroupsLikertContainer', () => {
});

await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('Feltet er påkrevd');
expect(screen.getByText(/feltet er påkrevd/i)).toBeInTheDocument();
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ describe('RepeatingGroupContainer', () => {
})[1],
);

await waitFor(() => expect(screen.getByRole('alert')).toHaveTextContent('Feltet er feil'));
await waitFor(() => expect(screen.getByText(/feltet er feil/i)).toBeInTheDocument());
});

it('should NOT trigger validate when saving if validation trigger is NOT present', async () => {
Expand Down Expand Up @@ -267,7 +267,7 @@ describe('RepeatingGroupContainer', () => {
})[1],
);

expect(screen.queryByRole('alert')).not.toBeInTheDocument();
expect(screen.queryByText(/feltet er feil/i)).not.toBeInTheDocument();
});

it('should display "Add new" button when edit.addButton is undefined', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@ describe('Expression validation', () => {
cy.findByRole('textbox', { name: /etternavn/i }).type('Hansen');

cy.findByRole('textbox', { name: /alder/i }).type('17');
cy.findByRole('alert', { name: /skriftlige samtykke/i }).should('be.visible');
cy.findByText(/skriftlige samtykke/i).should('be.visible');
cy.get(appFrontend.errorReport).should('not.exist');
cy.findByRole('textbox', { name: /alder/i }).clear();
cy.findByRole('textbox', { name: /alder/i }).type('14');
cy.findByRole('alert', { name: /skriftlige samtykke/i }).should('not.exist');
cy.findByText(/skriftlige samtykke/i).should('not.exist');
cy.get(appFrontend.errorReport).should('contain.text', 'Minste gyldig tall er 15');
cy.findByRole('textbox', { name: /alder/i }).clear();
cy.findByRole('textbox', { name: /alder/i }).type('15');
cy.findByRole('alert', { name: /skriftlige samtykke/i }).should('be.visible');
cy.findByText(/skriftlige samtykke/i).should('be.visible');
cy.get(appFrontend.errorReport).should('not.exist');
cy.findByRole('textbox', { name: /alder/i }).clear();
cy.findByRole('textbox', { name: /alder/i }).type('18');
cy.findByRole('alert', { name: /skriftlige samtykke/i }).should('not.exist');
cy.findByText(/skriftlige samtykke/i).should('not.exist');
cy.get(appFrontend.errorReport).should('not.exist');

cy.dsSelect(appFrontend.expressionValidationTest.kjønn, 'Mann');
Expand Down Expand Up @@ -64,7 +64,7 @@ describe('Expression validation', () => {

cy.findByRole('button', { name: /send inn/i }).click();
cy.get(appFrontend.errorReport).should('be.visible');
cy.findByRole('alert', { name: /skriftlige samtykke/i }).should('be.visible');
cy.findByText(/skriftlige samtykke/i).should('be.visible');
cy.gotoNavPage('Skjul felter');

cy.get(appFrontend.errorReport).should('contain.text', 'Du må fylle ut fornavn');
Expand Down
Loading
Loading