(frontend) make delete buttons nvda-accessible

add aria-labels and include close button in title prop so NVDA announces actions

Signed-off-by: Cyril <c.gromoff@gmail.com>
This commit is contained in:
Cyril
2025-08-11 11:18:30 +02:00
parent c9a6c4d4c6
commit 084d0c1089
15 changed files with 168 additions and 101 deletions

View File

@@ -28,6 +28,7 @@ and this project adheres to
- ♿(frontend) improve header accessibility #1270
- ♿(frontend) improve accessibility for decorative images in editor #1282
- #1338
- #1281
- ♻️(backend) fallback to email identifier when no name #1298
- 🐛(backend) allow ASCII characters in user sub field #1295
- ⚡️(frontend) improve fallback width calculation #1333

View File

@@ -29,12 +29,7 @@ test.describe('Doc Export', () => {
})
.click();
await expect(
page
.locator('div')
.filter({ hasText: /^Download$/ })
.first(),
).toBeVisible();
await expect(page.getByTestId('modal-export-title')).toBeVisible();
await expect(
page.getByText('Download your document in a .docx or .pdf format.'),
).toBeVisible();
@@ -45,7 +40,7 @@ test.describe('Doc Export', () => {
await expect(
page.getByRole('button', { name: 'Close the modal' }),
).toBeVisible();
await expect(page.getByRole('button', { name: 'Download' })).toBeVisible();
await expect(page.getByTestId('modal-download-button')).toBeVisible();
});
test('it exports the doc with pdf line break', async ({
@@ -136,23 +131,13 @@ test.describe('Doc Export', () => {
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'Docx' }).click();
await expect(
page.getByRole('button', {
name: 'Download',
exact: true,
}),
).toBeVisible();
await expect(page.getByTestId('modal-download-button')).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.docx`);
});
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
void page.getByTestId('modal-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.docx`);
@@ -218,11 +203,7 @@ test.describe('Doc Export', () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
await expect(
page.getByRole('button', {
name: 'Download',
}),
).toBeVisible();
await expect(page.getByTestId('modal-download-button')).toBeVisible();
const responseCorsPromise = page.waitForResponse(
(response) =>
@@ -233,11 +214,7 @@ test.describe('Doc Export', () => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page
.getByRole('button', {
name: 'Download',
})
.click();
void page.getByTestId('modal-download-button').click();
const responseCors = await responseCorsPromise;
expect(responseCors.ok()).toBe(true);
@@ -279,21 +256,13 @@ test.describe('Doc Export', () => {
})
.click();
await expect(
page.getByRole('button', {
name: 'Download',
}),
).toBeVisible();
await expect(page.getByTestId('modal-download-button')).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page
.getByRole('button', {
name: 'Download',
})
.click();
void page.getByTestId('modal-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
@@ -329,23 +298,13 @@ test.describe('Doc Export', () => {
})
.click();
await expect(
page.getByRole('button', {
name: 'Download',
exact: true,
}),
).toBeVisible();
await expect(page.getByTestId('modal-download-button')).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
void page.getByTestId('modal-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
@@ -391,23 +350,13 @@ test.describe('Doc Export', () => {
})
.click();
await expect(
page.getByRole('button', {
name: 'Download',
exact: true,
}),
).toBeVisible();
await expect(page.getByTestId('modal-download-button')).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
void page.getByTestId('modal-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
@@ -469,12 +418,7 @@ test.describe('Doc Export', () => {
return download.suggestedFilename().includes(`${randomDocFrench}.pdf`);
});
void page
.getByRole('button', {
name: 'Télécharger',
exact: true,
})
.click();
void page.getByTestId('modal-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDocFrench}.pdf`);
@@ -536,12 +480,7 @@ test.describe('Doc Export', () => {
})
.click();
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
void page.getByTestId('modal-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${docChild}.pdf`);

View File

@@ -155,7 +155,9 @@ test.describe('Doc Header', () => {
await page.getByRole('button', { name: 'Share' }).click();
const shareModal = page.getByLabel('Share modal');
const shareModal = page.getByRole('dialog', {
name: 'Share modal content',
});
await expect(shareModal).toBeVisible();
await expect(page.getByText('Share the document')).toBeVisible();
@@ -581,7 +583,10 @@ test.describe('Documents Header mobile', () => {
await page.getByLabel('Open the document options').click();
await page.getByLabel('Share').click();
await expect(page.getByLabel('Share modal')).toBeVisible();
const shareModal = page.getByRole('dialog', {
name: 'Share modal content',
});
await expect(shareModal).toBeVisible();
await page.getByRole('button', { name: 'close' }).click();
await expect(page.getByLabel('Share modal')).toBeHidden();
});

View File

@@ -72,6 +72,7 @@ test.describe('Home page', () => {
await page.waitForLoadState('domcontentloaded');
// Wait a bit more for the responsive store to be initialized
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(500);
// Check header content

View File

@@ -1,4 +1,4 @@
export * from './AlertModal';
export * from './modal/AlertModal';
export * from './Box';
export * from './BoxButton';
export * from './Card';
@@ -9,7 +9,7 @@ export * from './Icon';
export * from './InfiniteScroll';
export * from './Link';
export * from './Loading';
export * from './SideModal';
export * from './modal/SideModal';
export * from './separators';
export * from './Text';
export * from './TextErrors';

View File

@@ -2,8 +2,8 @@ import { Button, Modal, ModalSize } from '@openfun/cunningham-react';
import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { Box } from './Box';
import { Text } from './Text';
import { Box } from '../Box';
import { Text } from '../Text';
export type AlertModalProps = {
description: ReactNode;

View File

@@ -0,0 +1,22 @@
import { Button, type ButtonProps } from '@openfun/cunningham-react';
import React from 'react';
import { Box } from '@/components';
const ButtonCloseModal = (props: ButtonProps) => {
return (
<Button
type="button"
size="small"
color="primary-text"
icon={
<Box as="span" aria-hidden="true" className="material-icons-filled">
close
</Box>
}
{...props}
/>
);
};
export default ButtonCloseModal;

View File

@@ -16,6 +16,7 @@ import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, Text } from '@/components';
import ButtonCloseModal from '@/components/modal/ButtonCloseModal';
import { useEditorStore } from '@/docs/doc-editor';
import { Doc, useTrans } from '@/docs/doc-management';
@@ -131,6 +132,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
isOpen
closeOnClickOutside
onClose={() => onClose()}
hideCloseButton
rightActions={
<>
<Button
@@ -148,6 +150,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
fullWidth
onClick={() => void onSubmit()}
disabled={isExporting}
data-testid="modal-download-button"
>
{t('Download')}
</Button>
@@ -155,9 +158,26 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
}
size={ModalSize.MEDIUM}
title={
<Text $size="h6" $variation="1000" $align="flex-start">
{t('Download')}
</Text>
<Box
$direction="row"
$justify="space-between"
$align="center"
$width="100%"
>
<Text
$size="h6"
$variation="1000"
$align="flex-start"
data-testid="modal-export-title"
>
{t('Download')}
</Text>
<ButtonCloseModal
aria-label={t('Close the download modal')}
onClick={() => onClose()}
disabled={isExporting}
/>
</Box>
}
>
<Box

View File

@@ -10,6 +10,7 @@ import { useRouter } from 'next/router';
import { Trans, useTranslation } from 'react-i18next';
import { Box, Text, TextErrors } from '@/components';
import ButtonCloseModal from '@/components/modal/ButtonCloseModal';
import { useRemoveDoc } from '../api/useRemoveDoc';
import { Doc } from '../types';
@@ -53,11 +54,12 @@ export const ModalRemoveDoc = ({
<Modal
isOpen
closeOnClickOutside
hideCloseButton
onClose={() => onClose()}
rightActions={
<>
<Button
aria-label={t('Close the modal')}
aria-label={t('Close the delete modal')}
color="secondary"
fullWidth
onClick={() => onClose()}
@@ -80,15 +82,26 @@ export const ModalRemoveDoc = ({
}
size={ModalSize.MEDIUM}
title={
<Text
$size="h6"
as="h6"
$margin={{ all: '0' }}
$align="flex-start"
$variation="1000"
<Box
$direction="row"
$justify="space-between"
$align="center"
$width="100%"
>
{t('Delete a doc')}
</Text>
<Text
$size="h6"
as="h6"
$margin={{ all: '0' }}
$align="flex-start"
$variation="1000"
>
{t('Delete a doc')}
</Text>
<ButtonCloseModal
aria-label={t('Close the delete modal')}
onClick={() => onClose()}
/>
</Box>
}
>
<Box

View File

@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
import { useDebouncedCallback } from 'use-debounce';
import { Box } from '@/components';
import ButtonCloseModal from '@/components/modal/ButtonCloseModal';
import { QuickSearch } from '@/components/quick-search';
import { Doc, useDocUtils } from '@/docs/doc-management';
import { useResponsiveStore } from '@/stores';
@@ -63,6 +64,7 @@ const DocSearchModalGlobal = ({
{...modalProps}
closeOnClickOutside
size={isDesktop ? ModalSize.LARGE : ModalSize.FULL}
hideCloseButton
>
<Box
aria-label={t('Search modal')}
@@ -70,6 +72,14 @@ const DocSearchModalGlobal = ({
$justify="space-between"
className="--docs--doc-search-modal"
>
<Box $position="absolute" $css="top: 12px; right: 12px;">
<ButtonCloseModal
aria-label={t('Close the search modal')}
onClick={modalProps.onClose}
size="small"
color="primary-text"
/>
</Box>
<QuickSearch
placeholder={t('Type the name of a document')}
loading={loading}

View File

@@ -100,6 +100,7 @@ const DocShareAccessRequestItem = ({ doc, accessRequest }: Props) => {
docId: doc.id,
})
}
aria-label={t('Close the access request modal')}
>
<Icon iconName="close" $variation="600" $size="16px" />
</BoxButton>

View File

@@ -5,6 +5,7 @@ import { createGlobalStyle, css } from 'styled-components';
import { useDebouncedCallback } from 'use-debounce';
import { Box, HorizontalSeparator, Text } from '@/components';
import ButtonCloseModal from '@/components/modal/ButtonCloseModal';
import {
QuickSearch,
QuickSearchData,
@@ -137,12 +138,23 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
aria-label={t('Share modal')}
size={isDesktop ? ModalSize.LARGE : ModalSize.FULL}
onClose={onClose}
title={<Box $align="flex-start">{t('Share the document')}</Box>}
title={
<Box $direction="row" $justify="space-between" $align="center">
<Box $align="flex-start">{t('Share the document')}</Box>
<ButtonCloseModal
aria-label={t('Close the share modal')}
onClick={onClose}
/>
</Box>
}
hideCloseButton
>
<ShareModalStyle />
<Box
aria-label={t('Share modal')}
$height={canViewAccesses ? modalContentHeight : 'auto'}
role="dialog"
aria-label={t('Share modal content')}
$height="auto"
$maxHeight={canViewAccesses ? modalContentHeight : 'none'}
$overflow="hidden"
className="--docs--doc-share-modal noPadding "
$justify="space-between"

View File

@@ -3,7 +3,8 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { createGlobalStyle, css } from 'styled-components';
import { Box, Icon, Text } from '@/components';
import { Box, Text } from '@/components';
import ButtonCloseModal from '@/components/modal/ButtonCloseModal';
import { DocEditor } from '@/docs/doc-editor';
import { Doc } from '@/docs/doc-management';
@@ -114,11 +115,10 @@ export const ModalSelectVersion = ({
<Text $size="h6" $variation="1000" $weight="bold">
{t('History')}
</Text>
<Button
<ButtonCloseModal
aria-label={t('Close the version history modal')}
onClick={onClose}
size="nano"
color="primary-text"
icon={<Icon iconName="close" />}
/>
</Box>

View File

@@ -183,6 +183,7 @@
"Select language": "Dibab ur yezh",
"Share": "Rannañ",
"Share modal": "Modal rannañ",
"Share modal content": "Endalc'h modal rannañ",
"Share the document": "Rannañ an teul",
"Share with {{count}} users_many": "Rannañ gant {{count}} implijer",
"Share with {{count}} users_one": "Rannañ gant {{count}} implijer",
@@ -266,6 +267,11 @@
"Can't load this page, please check your internet connection.": "Diese Seite kann nicht geladen werden. Bitte überprüfen Sie Ihre Internetverbindung.",
"Cancel": "Abbrechen",
"Close the modal": "Pop-up schließen",
"Close the access request modal": "Zugriffsanfrage-Modal schließen",
"Close the delete modal": "Lösch-Modal schließen",
"Close the search modal": "Such-Modal schließen",
"Close the share modal": "Freigabe-Modal schließen",
"Close the version history modal": "Versionsverlauf-Modal schließen",
"Collaborate": "Zusammenarbeiten",
"Collaborate and write in real time, without layout constraints.": "In Echtzeit und ohne Layout-Beschränkungen schreiben und zusammenarbeiten.",
"Collaborative writing, Simplified.": "Kollaboratives Schreiben, vereinfacht.",
@@ -393,6 +399,7 @@
"Select a version on the right to restore": "Wählen Sie rechts eine Version zum Wiederherstellen aus",
"Share": "Teilen",
"Share modal": "Teilen-Modal",
"Share modal content": "Teilen-Modal Inhalt",
"Share the document": "Dokument teilen",
"Share with {{count}} users_many": "Teilen mit {{count}} Benutzern",
"Share with {{count}} users_one": "Mit {{count}} Benutzern teilen",
@@ -443,6 +450,17 @@
},
"en": {
"translation": {
"Back to homepage": "Back to Docs homepage",
"Search docs": "Search docs",
"More options": "More options",
"Pinned documents": "Pinned documents",
"Close the access request modal": "Close the access request modal",
"Close the delete modal": "Close the delete modal",
"Close the search modal": "Close the search modal",
"Close the share modal": "Close the share modal",
"Close the version history modal": "Close the version history modal",
"Open actions menu for document: {{title}}": "Open actions menu for document: {{title}}",
"Open the menu of actions for the document: {{title}}": "Open the menu of actions for the document: {{title}}",
"Share with {{count}} users_one": "Share with {{count}} user",
"Shared with {{count}} users_many": "Shared with {{count}} users",
"Shared with {{count}} users_one": "Shared with {{count}} user",
@@ -478,6 +496,11 @@
"Can't load this page, please check your internet connection.": "No se puede cargar esta página, por favor compruebe su conexión a Internet.",
"Cancel": "Cancelar",
"Close the modal": "Cerrar modal",
"Close the access request modal": "Cerrar modal de solicitud de acceso",
"Close the delete modal": "Cerrar modal de eliminación",
"Close the search modal": "Cerrar modal de búsqueda",
"Close the share modal": "Cerrar modal de compartir",
"Close the version history modal": "Cerrar modal de historial de versiones",
"Collaborate": "Colabora",
"Collaborate and write in real time, without layout constraints.": "Colaborar y escribir en tiempo real, sin restricciones de diseño.",
"Collaborative writing, Simplified.": "Escritura colaborativa, más sencilla.",
@@ -587,6 +610,7 @@
"Select a version on the right to restore": "Seleccione una versión a la derecha para restaurarlo",
"Share": "Compartir",
"Share modal": "Modal para compartir el documento",
"Share modal content": "Contenido del modal para compartir",
"Share the document": "Compartir el documento",
"Share with {{count}} users_many": "Compartir con {{count}} usuarios",
"Share with {{count}} users_one": "Comparte con {{count}} usuario",
@@ -662,6 +686,12 @@
"Can't load this page, please check your internet connection.": "Impossible de charger cette page, veuillez vérifier votre connexion Internet.",
"Cancel": "Annuler",
"Close the modal": "Fermer la modale",
"Close the share modal": "Fermer la modale de partage de document",
"Close the access request modal": "Fermer la modale de demande d'accès",
"Close the delete modal": "Fermer la modale de suppression",
"Close the download modal": "Fermer la modale de téléchargement",
"Close the search modal": "Fermer la modale de recherche de document",
"Close the version history modal": "Fermer la modale d'historique des versions",
"Collaborate": "Collaborer",
"Collaborate and write in real time, without layout constraints.": "Collaborez et rédigez en temps réel, sans contrainte de mise en page.",
"Collaborative writing, Simplified.": "L'écriture collaborative simplifiée.",
@@ -813,6 +843,7 @@
"Select language": "Sélectionner la langue",
"Share": "Partager",
"Share modal": "Modale de partage",
"Share modal content": "Contenu de la modale de partage",
"Share the document": "Partager le document",
"Share with {{count}} users_many": "Partagé entre {{count}} utilisateurs",
"Share with {{count}} users_one": "Partager avec {{count}} utilisateur",
@@ -829,6 +860,10 @@
"Summarize": "Résumer",
"Summary": "Sommaire",
"Template": "Modèle",
"Document template": "Modèle de document",
"File format": "Format de fichier",
"Choose a template to apply to your document before download": "Choisissez un modèle à appliquer à votre document avant le téléchargement",
"Choose the file format for your download": "Choisissez le format de fichier pour votre téléchargement",
"The antivirus has detected an anomaly in your file.": "L'antivirus a détecté une anomalie dans votre fichier.",
"The document has been deleted.": "Le document a bien été supprimé.",
"The document visibility has been updated.": "La visibilité du document a été mise à jour.",
@@ -1039,6 +1074,11 @@
"Can't load this page, please check your internet connection.": "Kan deze pagina niet laden. Controleer je internetverbinding.",
"Cancel": "Breek af",
"Close the modal": "Sluit het venster",
"Close the access request modal": "Sluit het toegangsverzoek venster",
"Close the delete modal": "Sluit het verwijder venster",
"Close the search modal": "Sluit het zoek venster",
"Close the share modal": "Sluit het delen venster",
"Close the version history modal": "Sluit het versiehistorie venster",
"Collaborate": "Samenwerken",
"Collaborate and write in real time, without layout constraints.": "Samenwerken en schrijven in realtime, zonder lay-out beperkingen.",
"Collaborative writing, Simplified.": "Vereenvoudigd samenwerkend",
@@ -1145,6 +1185,7 @@
"Select a version on the right to restore": "Selecteer een versie rechts om te herstellen",
"Share": "Deel",
"Share modal": "Deel model",
"Share modal content": "Deel model inhoud",
"Share the document": "Deel dit document",
"Share with {{count}} users_many": "Gedeeld met {{count}} gebruikers",
"Share with {{count}} users_one": "Delen met {{count}} gebruiker",
@@ -1160,7 +1201,7 @@
"Start Writing": "Begin met schrijven",
"Summarize": "Vat samen",
"Summary": "Samenvatting",
"Template": "Template",
"Template": "Sjabloon",
"The document has been deleted.": "Het document is verwijderd",
"The document visibility has been updated.": "De zichtbaarheid van het document is bijgewerkt",
"The export failed": "Het exporteren is mislukt",
@@ -1804,6 +1845,7 @@
"Can't load this page, please check your internet connection.": "无法加载此页面,请检查您的网络连接。",
"Cancel": "取消",
"Close the modal": "关闭对话框",
"Close the share modal": "关闭分享文档对话框",
"Collaborate": "协作",
"Collaborate and write in real time, without layout constraints.": "实时协作和写入,不受布局限制。",
"Collaborative writing, Simplified.": "协作写入,简体字样。",
@@ -1910,6 +1952,7 @@
"Select a version on the right to restore": "选择要恢复的版本",
"Share": "共享",
"Share modal": "共享模式",
"Share modal content": "共享模式内容",
"Share the document": "共享文档",
"Share with {{count}} users_many": "与{{count}}共享",
"Share with {{count}} users_one": "与{{count}}共享",