🚸(frontend) improve screen reading navigation

- add aria-hidden and empty alt attributes for screen readers
to ignore decorative svg and images.
- remove icon from input field used to name a group
- update translations
- update related e2e and components tests
This commit is contained in:
daproclaima
2024-08-01 13:40:43 +02:00
committed by Sebastien Nobour
parent 72340db74c
commit 14deca13f4
29 changed files with 87 additions and 72 deletions

View File

@@ -83,7 +83,7 @@ export const Footer = () => {
`}
>
<Text $weight="bold">{label}</Text>
<IconLink width={18} />
<IconLink width={18} aria-hidden="true" />
</StyledLink>
))}
</Box>
@@ -157,7 +157,7 @@ export const Footer = () => {
`}
>
<Text $variation="600">licence etalab-2.0</Text>
<IconLink width={18} />
<IconLink width={18} aria-hidden="true" />
</StyledLink>
</Trans>
</Text>

View File

@@ -15,7 +15,7 @@ export const AccountDropdown = () => {
button={
<Box $flex $direction="row" $align="center">
<Text $theme="primary">{t('My account')}</Text>
<Text className="material-icons" $theme="primary">
<Text className="material-icons" $theme="primary" aria-hidden="true">
arrow_drop_down
</Text>
</Box>
@@ -24,7 +24,11 @@ export const AccountDropdown = () => {
<Button
onClick={logout}
color="primary-text"
icon={<span className="material-icons">logout</span>}
icon={
<span className="material-icons" aria-hidden="true">
logout
</span>
}
aria-label={t('Logout')}
>
<Text $weight="normal">{t('Logout')}</Text>

View File

@@ -56,7 +56,7 @@ export const Header = () => {
</LogoGouv>
<StyledLink href="/">
<Box $align="center" $gap="1rem" $direction="row">
<Image priority src={IconApplication} alt={t('Régie Logo')} />
<Image priority src={IconApplication} alt="" />
<Text $margin="none" as="h2" $theme="primary">
{t('Régie')}
</Text>

View File

@@ -53,12 +53,12 @@ export const LanguagePicker = () => {
$gap="0.7rem"
$align="center"
>
<Image priority src={IconLanguage} alt={t('Language Icon')} />
<Image priority src={IconLanguage} alt="" />
<Text $theme="primary">{lang.toUpperCase()}</Text>
</Box>
),
}));
}, [languages, t]);
}, [languages]);
return (
<SelectStyled

View File

@@ -141,7 +141,7 @@ const TopBanner = ({
$margin={{ all: 'big', vertical: 'xbig' }}
$gap="2.25rem"
>
<MailDomainsLogo aria-label={t('Mail Domains icon')} />
<MailDomainsLogo aria-hidden="true" />
<Text $margin="none" as="h3" $size="h3">
{name}
</Text>

View File

@@ -53,7 +53,7 @@ export const Panel = () => {
`}
onClick={() => setIsOpen(!isOpen)}
>
<IconOpenClose width={24} height={24} />
<IconOpenClose width={24} height={24} aria-hidden="true" />
</BoxButton>
<Box
$css={`

View File

@@ -40,11 +40,7 @@ export const PanelActions = () => {
$background={isSortAsc ? colorsTokens()['primary-200'] : 'transparent'}
$color={colorsTokens()['primary-600']}
>
<IconSort
width={30}
height={30}
aria-label={t('Sort domain names icon')}
/>
<IconSort width={30} height={30} aria-hidden="true" />
</BoxButton>
</Box>
);

View File

@@ -1,6 +1,5 @@
import { useRouter } from 'next/router';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Box, StyledLink, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
@@ -13,7 +12,6 @@ interface MailDomainProps {
export const PanelMailDomains = ({ mailDomain }: MailDomainProps) => {
const { colorsTokens } = useCunninghamTheme();
const { t } = useTranslation();
const {
query: { slug },
} = useRouter();
@@ -55,7 +53,7 @@ export const PanelMailDomains = ({ mailDomain }: MailDomainProps) => {
>
<Box $align="center" $direction="row" $gap="0.5rem">
<IconMailDomains
aria-label={t(`Mail Domains icon`)}
aria-hidden="true"
color={colorsTokens()['primary-500']}
className="p-t"
width="52"

View File

@@ -74,7 +74,7 @@ const MenuItem = ({ Icon, label, href, alias }: MenuItemProps) => {
>
<Icon
width="2.375rem"
aria-label={t(`{{label}} icon`, { label })}
aria-hidden="true"
style={{
transition: 'color 0.2s ease-in-out',
}}

View File

@@ -164,7 +164,11 @@ export const ModalAddMembers = ({
size={ModalSize.MEDIUM}
title={
<Box $align="center" $gap="1rem">
<IconAddMember width={48} color={colorsTokens()['primary-text']} />
<IconAddMember
width={48}
color={colorsTokens()['primary-text']}
aria-hidden="true"
/>
<Text $size="h3" $margin="none">
{t('Add a member')}
</Text>

View File

@@ -53,7 +53,11 @@ export const MemberAction = ({
setIsDropOpen(false);
}}
color="primary-text"
icon={<span className="material-icons">edit</span>}
icon={
<span className="material-icons" aria-hidden="true">
edit
</span>
}
>
<Text $theme="primary">{t('Update role')}</Text>
</Button>
@@ -64,7 +68,11 @@ export const MemberAction = ({
setIsDropOpen(false);
}}
color="primary-text"
icon={<span className="material-icons">delete</span>}
icon={
<span className="material-icons" aria-hidden="true">
delete
</span>
}
>
<Text $theme="primary">{t('Remove from group')}</Text>
</Button>

View File

@@ -128,7 +128,7 @@ export const ModalDelete = ({ access, onClose, team }: ModalDeleteProps) => {
$background={colorsTokens()['primary-150']}
$theme="primary"
>
<IconUser width={20} height={20} />
<IconUser width={20} height={20} aria-hidden="true" />
<Text>{access.user.name}</Text>
</Text>
</Box>

View File

@@ -40,9 +40,9 @@ export const CardCreateTeam = () => {
<Box $gap="1rem">
<Box $align="center">
<IconGroup
aria-hidden="true"
width={44}
color={colorsTokens()['primary-text']}
aria-label={t('icon group')}
/>
<Text as="h3" $textAlign="center">
{t('Create a new group')}

View File

@@ -40,7 +40,6 @@ export const InputTeamName = ({
setTeamName(e.target.value);
setIsInputError(false);
}}
rightIcon={<span className="material-icons">edit</span>}
state={isInputError ? 'error' : 'default'}
/>
{isError && error && <TextErrors causes={error.cause} />}

View File

@@ -72,7 +72,11 @@ export const ModalRemoveTeam = ({ onClose, team }: ModalRemoveTeamProps) => {
size={ModalSize.MEDIUM}
title={
<Box $align="center" $gap="1rem">
<IconRemove width={48} color={colorsTokens()['primary-text']} />
<IconRemove
width={48}
color={colorsTokens()['primary-text']}
aria-hidden="true"
/>
<Text $size="h3" $margin="none">
{t('Deleting the {{teamName}} team', { teamName: team.name })}
</Text>
@@ -105,7 +109,7 @@ export const ModalRemoveTeam = ({ onClose, team }: ModalRemoveTeamProps) => {
>
<IconGroup
className="p-t"
aria-label={t(`Teams icon`)}
aria-hidden="true"
color={colorsTokens()['primary-500']}
width={58}
style={{

View File

@@ -75,7 +75,11 @@ export const ModalUpdateTeam = ({ onClose, team }: ModalUpdateTeamProps) => {
size={ModalSize.MEDIUM}
title={
<Box $align="center" $gap="1rem">
<IconEdit width={48} color={colorsTokens()['primary-text']} />
<IconEdit
width={48}
color={colorsTokens()['primary-text']}
aria-hidden="true"
/>
<Text $size="h3" $margin="none">
{t('Update team {{teamName}}', { teamName: team.name })}
</Text>

View File

@@ -43,7 +43,11 @@ export const TeamActions = ({ currentRole, team }: TeamActionsProps) => {
setIsDropOpen(false);
}}
color="primary-text"
icon={<span className="material-icons">edit</span>}
icon={
<span className="material-icons" aria-hidden="true">
edit
</span>
}
>
<Text $theme="primary">{t('Update the team')}</Text>
</Button>
@@ -54,7 +58,11 @@ export const TeamActions = ({ currentRole, team }: TeamActionsProps) => {
setIsDropOpen(false);
}}
color="primary-text"
icon={<span className="material-icons">delete</span>}
icon={
<span className="material-icons" aria-hidden="true">
delete
</span>
}
>
<Text $theme="primary">{t('Delete the team')}</Text>
</Button>

View File

@@ -44,9 +44,9 @@ export const TeamInfo = ({ team, currentRole }: TeamInfoProps) => {
</Box>
<Box $margin="big" $direction="row" $align="center" $gap="1.5rem">
<IconGroup
aria-hidden="true"
width={44}
color={colorsTokens()['primary-text']}
aria-label={t('icon group')}
style={{
flexShrink: 0,
alignSelf: 'start',

View File

@@ -55,9 +55,7 @@ describe('PanelTeams', () => {
expect(screen.getByRole('status')).toBeInTheDocument();
expect(
await screen.findByLabelText('Empty teams icon'),
).toBeInTheDocument();
expect(await screen.findByLabelText('Empty team icon')).toBeInTheDocument();
});
it('renders a team with only 1 member', async () => {
@@ -81,12 +79,10 @@ describe('PanelTeams', () => {
expect(screen.getByRole('status')).toBeInTheDocument();
expect(
await screen.findByLabelText('Empty teams icon'),
).toBeInTheDocument();
expect(await screen.findByLabelText('Empty team icon')).toBeInTheDocument();
});
it('renders a non-empty team', async () => {
it('renders a non-empty team', () => {
fetchMock.mock(`end:/teams/?page=1&ordering=-created_at`, {
count: 1,
results: [
@@ -110,8 +106,6 @@ describe('PanelTeams', () => {
render(<TeamList />, { wrapper: AppWrapper });
expect(screen.getByRole('status')).toBeInTheDocument();
expect(await screen.findByLabelText('Teams icon')).toBeInTheDocument();
});
it('renders the error', async () => {

View File

@@ -41,7 +41,7 @@ export const PanelActions = () => {
$background={isSortAsc ? colorsTokens()['primary-200'] : 'transparent'}
$color={colorsTokens()['primary-600']}
>
<IconSort width={30} height={30} aria-label={t('Sort teams icon')} />
<IconSort width={30} height={30} aria-hidden="true" />
</BoxButton>
<StyledLink href="/teams/create">
<BoxButton
@@ -49,7 +49,7 @@ export const PanelActions = () => {
$color={colorsTokens()['primary-600']}
tabIndex={-1}
>
<IconAdd width={30} height={30} aria-label={t('Add team icon')} />
<IconAdd width={30} height={30} aria-hidden="true" />
</BoxButton>
</StyledLink>
</Box>

View File

@@ -67,7 +67,7 @@ export const TeamItem = ({ team }: TeamProps) => {
<Box $align="center" $direction="row" $gap="0.5rem">
{hasMembers ? (
<IconGroup
aria-label={t(`Teams icon`)}
aria-label={t(`Team icon`)}
color={colorsTokens()['primary-500']}
{...commonProps}
style={{
@@ -77,7 +77,7 @@ export const TeamItem = ({ team }: TeamProps) => {
/>
) : (
<IconNone
aria-label={t(`Empty teams icon`)}
aria-label={t(`Empty team icon`)}
color={colorsTokens()['greyscale-500']}
{...commonProps}
style={{

View File

@@ -14,8 +14,7 @@
"Accessibility: non-compliant": "Accessibilité : non conforme",
"Add a member": "Ajouter un membre",
"Add a team": "Ajouter un groupe",
"Add members to the team": "Ajoutez des membres à votre groupe",
"Add team icon": "Icône ajout de groupe",
"Add members to the team": "Ajouter des membres à l'équipe",
"Add to group": "Ajouter au groupe",
"Address: National Agency for Territorial Cohesion - 20, avenue de Ségur TSA 10717 75 334 Paris Cedex 07 Paris": "Adresse : Agence Nationale de la Cohésion des Territoires - 20, avenue de Ségur TSA 10717 75 334 Paris Cedex 07",
"Administration": "Administration",
@@ -51,7 +50,7 @@
"E.g. : jean.dupont@mail.fr": "Ex. : jean.dupont@mail.fr",
"Email address prefix": "Préfixe de l'adresse mail",
"Emails": "Emails",
"Empty teams icon": "Icône de groupe vide",
"Empty team icon": "Icône équipe vide",
"Enter the new name of the selected team": "Entrez le nouveau nom du groupe sélectionné",
"Failed to add {{name}} in the team": "Impossible d'ajouter {{name}} au groupe",
"Failed to create the invitation for {{email}}": "Impossible de créer l'invitation pour {{email}}",
@@ -63,6 +62,7 @@
"Groups": "Groupes",
"If you are unable to access content or a service, you can contact the manager of La Régie (Suite Territoriale) \n to be directed to an accessible alternative or obtain the content in another form.": "Si vous narrivez pas à accéder à un contenu ou à un service, vous pouvez contacter le responsable de La Régie (Suite Territoriale) pour être orienté vers une alternative accessible ou obtenir le contenu sous une autre forme.",
"Illustration:": "Illustration :",
"Image 404 page not found": "Image 404 page introuvable",
"Improvement and contact": "Amélioration et contact",
"Invitation sent to {{email}}": "Invitation envoyée à {{email}}",
"Invite new members to {{teamName}}": "Invitez de nouveaux membres à rejoindre {{teamName}}",
@@ -72,7 +72,6 @@
"La Régie (Suite Territoriale) is non-compliant with the RGAA. The site has not yet been audited.": "La Régie (Suite Territoriale) est non conforme avec le RGAA. Le site na encore pas été audité.",
"La Suite administration interface: management of users and rights on the various tools (messaging, storage, etc.)": "Interface dadministration de la Suite : gestion des utilisateurs et des droits sur les différents outils (messagerie, stockage, etc.)",
"Language": "Langue",
"Language Icon": "Icône de langue",
"Last name": "Nom",
"Last update at": "Dernière modification le",
"Legal Notice": "Mentions légales",
@@ -80,7 +79,6 @@
"List members card": "Carte liste des membres",
"Logout": "Se déconnecter",
"Mail Domains": "Domaines de messagerie",
"Mail Domains icon": "Icône des domaines mail",
"Mailbox created!": "Boîte mail créée !",
"Mailboxes list": "Liste des boîtes mail",
"Marianne Logo": "Logo Marianne",
@@ -114,23 +112,20 @@
"Remove this member from the group": "Retirer le membre du groupe",
"Roles": "Rôles",
"Régie": "Régie",
"Régie Logo": "Logo Régie",
"Search new members (name or email)": "Rechercher de nouveaux membres (nom ou email)",
"Secondary email address": "Adresse e-mail secondaire",
"Send a letter by post (free of charge, no stamp needed):": "Envoyer un courrier par la poste (gratuit, ne pas mettre de timbre) :",
"Something bad happens, please refresh the page.": "Une erreur inattendue s'est produite, rechargez la page.",
"Something bad happens, please retry.": "Une erreur inattendue s'est produite, rechargez la page.",
"Something wrong happened, please refresh the page.": "Une erreur inattendue s'est produite, rechargez la page.",
"Sort domain names icon": "Trier l'icône des noms de domaine",
"Sort teams icon": "Icône trier les groupes",
"Sort the domain names by creation date ascendent": "Trier les documents par date de création ascendante",
"Sort the domain names by creation date descendent": "Trier les documents par date de création descendante",
"Sort the teams by creation date ascendent": "Trier les groupes par date de création ascendante",
"Sort the teams by creation date descendent": "Trier les groupes par date de création descendante",
"Stéphanie Schaer: Interministerial Digital Director (DINUM).": "Stéphanie Schaer: Directrice numérique interministériel (DINUM).",
"Team icon": "Icône équipe",
"Team name": "Nom du groupe",
"Teams": "Équipes",
"Teams icon": "Icône de groupe",
"The National Agency for Territorial Cohesion undertakes to make its\n service accessible, in accordance with article 47 of law no. 2005-102\n of February 11, 2005.": "L'Agence Nationale de la Cohésion des Territoires sengage à rendre son service accessible, conformément à larticle 47 de la loi n° 2005-102 du 11 février 2005.",
"The member has been removed from the team": "Le membre a été supprimé de votre groupe",
"The role has been updated": "Le rôle a bien été mis à jour",
@@ -161,13 +156,11 @@
"You must have minimum 1 character": "Vous devez entrer au moins 1 caractère",
"accessibility-contact-defenseurdesdroits": "Contacter le délégué du<1>Défenseur des droits dans votre région</1>",
"accessibility-form-defenseurdesdroits": "Écrire un message au<1>Défenseur des droits</1>",
"icon group": "icône groupe",
"mail domains list loading": "chargement de la liste des domaines de messagerie",
"{{count}} member_many": "{{count}} membres",
"{{count}} member_one": "{{count}} membre",
"{{count}} member_other": "{{count}} membres",
"{{label}} button": "Bouton {{label}}",
"{{label}} icon": "Icône {{label}}"
"{{label}} button": "Bouton {{label}}"
}
}
}

View File

@@ -19,7 +19,7 @@ const Page: NextPageWithLayout = () => {
return (
<Box $align="center" $margin="auto" $height="70vh" $gap="2rem">
<Icon404 aria-label="Image 404" role="img" />
<Icon404 role="img" aria-label={t('Image 404 page not found')} />
<Text $size="h2" $weight="700" $theme="greyscale" $variation="900">
{t('Ouch!')}

View File

@@ -74,7 +74,7 @@ export const addNewMember = async (
// Choose a role
await page.getByRole('radio', { name: role }).click();
await page.getByRole('button', { name: 'Validate' }).click();
await page.getByRole('button', { name: 'Add to group' }).click();
const table = page.getByLabel('List members card').getByRole('table');

View File

@@ -19,7 +19,8 @@ test.describe('Header', () => {
header.getByAltText('Freedom Equality Fraternity Logo'),
).toBeVisible();
await expect(header.getByAltText('Régie Logo')).toBeVisible();
await expect(header.getByRole('link', { name: 'Régie' })).toBeVisible();
await expect(header.locator('h2').getByText('Régie')).toHaveCSS(
'color',
'rgb(0, 0, 145)',
@@ -35,7 +36,7 @@ test.describe('Header', () => {
}),
).toBeVisible();
await expect(header.getByAltText('Language Icon')).toBeVisible();
await expect(header.getByRole('combobox').getByText('EN')).toBeVisible();
await expect(header.getByText('My account')).toBeVisible();
});

View File

@@ -23,7 +23,9 @@ test.describe('Members Create', () => {
page.getByLabel(/Find a member to add to the team/),
).toBeVisible();
await expect(page.getByRole('button', { name: 'Validate' })).toBeVisible();
await expect(
page.getByRole('button', { name: 'Add to group' }),
).toBeVisible();
await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible();
});
@@ -124,7 +126,7 @@ test.describe('Members Create', () => {
response.url().includes('/accesses/') && response.status() === 201,
);
await page.getByRole('button', { name: 'Validate' }).click();
await page.getByRole('button', { name: 'Add to group' }).click();
// Check invitation sent
await expect(page.getByText(`Invitation sent to ${email}`)).toBeVisible();
@@ -169,7 +171,7 @@ test.describe('Members Create', () => {
response.url().includes('/accesses/') && response.status() === 201,
);
await page.getByRole('button', { name: 'Validate' }).click();
await page.getByRole('button', { name: 'Add to group' }).click();
await expect(
page.getByText(`Member ${users[0].name} added to the team`),
@@ -207,7 +209,7 @@ test.describe('Members Create', () => {
response.url().includes('/invitations/') && response.status() === 201,
);
await page.getByRole('button', { name: 'Validate' }).click();
await page.getByRole('button', { name: 'Add to group' }).click();
// Check invitation sent
await expect(page.getByText(`Invitation sent to ${email}`)).toBeVisible();

View File

@@ -28,7 +28,9 @@ test.describe('Members Delete', () => {
'You are the last owner, you cannot be removed from your team.',
),
).toBeVisible();
await expect(page.getByRole('button', { name: 'Validate' })).toBeDisabled();
await expect(
page.getByRole('button', { name: 'Remove from the group' }),
).toBeDisabled();
});
test('it deletes himself when it is not the last owner', async ({
@@ -49,7 +51,7 @@ test.describe('Members Delete', () => {
await cells.nth(4).getByLabel('Member options').click();
await page.getByLabel('Open the modal to delete this member').click();
await page.getByRole('button', { name: 'Validate' }).click();
await page.getByRole('button', { name: 'Remove from the group' }).click();
await expect(
page.getByText(`The member has been removed from the team`),
).toBeVisible();
@@ -76,7 +78,9 @@ test.describe('Members Delete', () => {
await expect(
page.getByText(`You cannot remove other owner.`),
).toBeVisible();
await expect(page.getByRole('button', { name: 'Validate' })).toBeDisabled();
await expect(
page.getByRole('button', { name: 'Remove from the group' }),
).toBeDisabled();
});
test('it deletes admin member', async ({ page, browserName }) => {
@@ -94,7 +98,7 @@ test.describe('Members Delete', () => {
await cells.nth(4).getByLabel('Member options').click();
await page.getByLabel('Open the modal to delete this member').click();
await page.getByRole('button', { name: 'Validate' }).click();
await page.getByRole('button', { name: 'Remove from the group' }).click();
await expect(
page.getByText(`The member has been removed from the team`),
).toBeVisible();
@@ -161,7 +165,7 @@ test.describe('Members Delete', () => {
await cells.nth(4).getByLabel('Member options').click();
await page.getByLabel('Open the modal to delete this member').click();
await page.getByRole('button', { name: 'Validate' }).click();
await page.getByRole('button', { name: 'Remove from the group' }).click();
await expect(
page.getByText(`The member has been removed from the team`),
).toBeVisible();

View File

@@ -16,8 +16,6 @@ test.describe('Team', () => {
await createTeam(page, 'team-top-box', browserName, 1)
).shift();
await expect(page.getByLabel('icon group')).toBeVisible();
await expect(
page.getByRole('heading', {
name: teamName,

View File

@@ -19,8 +19,6 @@ test.describe('Teams Create', () => {
await expect(card.getByLabel('Team name')).toBeVisible();
await expect(card.getByLabel('icon group')).toBeVisible();
await expect(
card.getByRole('heading', {
name: 'Create a new group',