(frontend) improve modal a11y: structure, labels, and title

added aria-label, structured text in p, and added title for better accessibility

Signed-off-by: Cyril <c.gromoff@gmail.com>
This commit is contained in:
Cyril
2025-09-08 11:58:06 +02:00
parent 9f9fae96e5
commit 8a310d004b
16 changed files with 114 additions and 39 deletions

View File

@@ -8,12 +8,13 @@ and this project adheres to
## [Unreleased] ## [Unreleased]
- ⚡️(frontend) improve accessibility:
- #1341
### Added ### Added
- ✨(api) add API route to fetch document content #1206 - ✨(api) add API route to fetch document content #1206
- ♿(frontend) improve accessibility:
- #1349
- #1271
- #1341
### Changed ### Changed
@@ -41,7 +42,6 @@ and this project adheres to
- ♿(frontend) improve accessibility for decorative images in editor #1282 - ♿(frontend) improve accessibility for decorative images in editor #1282
- #1338 - #1338
- #1281 - #1281
- #1271
- ♻️(backend) fallback to email identifier when no name #1298 - ♻️(backend) fallback to email identifier when no name #1298
- 🐛(backend) allow ASCII characters in user sub field #1295 - 🐛(backend) allow ASCII characters in user sub field #1295
- ⚡️(frontend) improve fallback width calculation #1333 - ⚡️(frontend) improve fallback width calculation #1333

View File

@@ -463,12 +463,14 @@ test.describe('Doc Editor', () => {
await expect( await expect(
page.getByRole('button', { page.getByRole('button', {
name: 'Download', name: 'Download',
exact: true,
}), }),
).toBeVisible(); ).toBeVisible();
void page void page
.getByRole('button', { .getByRole('button', {
name: 'Download', name: 'Download',
exact: true,
}) })
.click(); .click();

View File

@@ -38,7 +38,9 @@ test.describe('Doc Export', () => {
).toBeVisible(); ).toBeVisible();
await expect(page.getByRole('combobox', { name: 'Format' })).toBeVisible(); await expect(page.getByRole('combobox', { name: 'Format' })).toBeVisible();
await expect( await expect(
page.getByRole('button', { name: 'Close the modal' }), page.getByRole('button', {
name: 'Close the download modal',
}),
).toBeVisible(); ).toBeVisible();
await expect(page.getByTestId('doc-export-download-button')).toBeVisible(); await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
}); });

View File

@@ -149,7 +149,7 @@ test.describe('Document grid item options', () => {
await page await page
.getByRole('button', { .getByRole('button', {
name: 'Confirm deletion', name: 'Delete document',
}) })
.click(); .click();

View File

@@ -100,7 +100,7 @@ test.describe('Doc Header', () => {
await page await page
.getByRole('button', { .getByRole('button', {
name: 'Confirm deletion', name: 'Delete document',
}) })
.click(); .click();

View File

@@ -33,7 +33,7 @@ test.describe('Document search', () => {
).toBeVisible(); ).toBeVisible();
await expect( await expect(
page.getByLabel('Search modal').getByText('search'), page.getByRole('heading', { name: 'Search docs' }),
).toBeVisible(); ).toBeVisible();
const inputSearch = page.getByPlaceholder('Type the name of a document'); const inputSearch = page.getByPlaceholder('Type the name of a document');
@@ -79,7 +79,7 @@ test.describe('Document search', () => {
await page.keyboard.press('Control+k'); await page.keyboard.press('Control+k');
await expect( await expect(
page.getByLabel('Search modal').getByText('search'), page.getByRole('heading', { name: 'Search docs' }),
).toBeVisible(); ).toBeVisible();
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');

View File

@@ -30,15 +30,23 @@ export const AlertModal = ({
isOpen={isOpen} isOpen={isOpen}
size={ModalSize.MEDIUM} size={ModalSize.MEDIUM}
onClose={onClose} onClose={onClose}
aria-describedby="alert-modal-title"
title={ title={
<Text $size="h6" $align="flex-start" $variation="1000"> <Text
$size="h6"
as="h1"
$margin="0"
id="alert-modal-title"
$align="flex-start"
$variation="1000"
>
{title} {title}
</Text> </Text>
} }
rightActions={ rightActions={
<> <>
<Button <Button
aria-label={t('Close the modal')} aria-label={`${t('Cancel')} - ${title}`}
color="secondary" color="secondary"
fullWidth fullWidth
onClick={() => onClose()} onClick={() => onClose()}
@@ -55,12 +63,11 @@ export const AlertModal = ({
</> </>
} }
> >
<Box <Box className="--docs--alert-modal">
aria-label={t('Confirmation button')}
className="--docs--alert-modal"
>
<Box> <Box>
<Text $variation="600">{description}</Text> <Text $variation="600" as="p">
{description}
</Text>
</Box> </Box>
</Box> </Box>
</Modal> </Modal>

View File

@@ -19,10 +19,11 @@ export const ModalConfirmDownloadUnsafe = ({
isOpen isOpen
closeOnClickOutside closeOnClickOutside
onClose={() => onClose()} onClose={() => onClose()}
aria-describedby="modal-confirm-download-unsafe-title"
rightActions={ rightActions={
<> <>
<Button <Button
aria-label={t('Close the modal')} aria-label={t('Cancel the download')}
color="secondary" color="secondary"
onClick={() => onClose()} onClick={() => onClose()}
> >
@@ -31,6 +32,7 @@ export const ModalConfirmDownloadUnsafe = ({
<Button <Button
aria-label={t('Download')} aria-label={t('Download')}
color="danger" color="danger"
data-testid="modal-download-unsafe-button"
onClick={() => { onClick={() => {
if (onConfirm) { if (onConfirm) {
void onConfirm(); void onConfirm();
@@ -45,6 +47,8 @@ export const ModalConfirmDownloadUnsafe = ({
size={ModalSize.SMALL} size={ModalSize.SMALL}
title={ title={
<Text <Text
as="h1"
id="modal-confirm-download-unsafe-title"
$gap="0.7rem" $gap="0.7rem"
$size="h6" $size="h6"
$align="flex-start" $align="flex-start"

View File

@@ -133,10 +133,11 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
closeOnClickOutside closeOnClickOutside
onClose={() => onClose()} onClose={() => onClose()}
hideCloseButton hideCloseButton
aria-describedby="modal-export-title"
rightActions={ rightActions={
<> <>
<Button <Button
aria-label={t('Close the modal')} aria-label={t('Cancel the download')}
color="secondary" color="secondary"
fullWidth fullWidth
onClick={() => onClose()} onClick={() => onClose()}
@@ -165,6 +166,9 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
$width="100%" $width="100%"
> >
<Text <Text
as="h1"
$margin="0"
id="modal-export-title"
$size="h6" $size="h6"
$variation="1000" $variation="1000"
$align="flex-start" $align="flex-start"
@@ -186,7 +190,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
$gap="1rem" $gap="1rem"
className="--docs--modal-export-content" className="--docs--modal-export-content"
> >
<Text $variation="600" $size="sm"> <Text $variation="600" $size="sm" as="p">
{t('Download your document in a .docx or .pdf format.')} {t('Download your document in a .docx or .pdf format.')}
</Text> </Text>
<Select <Select

View File

@@ -56,10 +56,11 @@ export const ModalRemoveDoc = ({
closeOnClickOutside closeOnClickOutside
hideCloseButton hideCloseButton
onClose={() => onClose()} onClose={() => onClose()}
aria-describedby="modal-remove-doc-title"
rightActions={ rightActions={
<> <>
<Button <Button
aria-label={t('Close the delete modal')} aria-label={t('Cancel the deletion')}
color="secondary" color="secondary"
fullWidth fullWidth
onClick={() => onClose()} onClick={() => onClose()}
@@ -67,7 +68,7 @@ export const ModalRemoveDoc = ({
{t('Cancel')} {t('Cancel')}
</Button> </Button>
<Button <Button
aria-label={t('Confirm deletion')} aria-label={t('Delete document')}
color="danger" color="danger"
fullWidth fullWidth
onClick={() => onClick={() =>
@@ -90,8 +91,9 @@ export const ModalRemoveDoc = ({
> >
<Text <Text
$size="h6" $size="h6"
as="h6" as="h1"
$margin={{ all: '0' }} id="modal-remove-doc-title"
$margin="0"
$align="flex-start" $align="flex-start"
$variation="1000" $variation="1000"
> >
@@ -104,12 +106,9 @@ export const ModalRemoveDoc = ({
</Box> </Box>
} }
> >
<Box <Box className="--docs--modal-remove-doc">
aria-label={t('Content modal to delete document')}
className="--docs--modal-remove-doc"
>
{!isError && ( {!isError && (
<Text $size="sm" $variation="600" $display="inline-block"> <Text $size="sm" $variation="600" $display="inline-block" as="p">
<Trans t={t}> <Trans t={t}>
This document and <strong>any sub-documents</strong> will be This document and <strong>any sub-documents</strong> will be
permanently deleted. This action is irreversible. permanently deleted. This action is irreversible.

View File

@@ -5,7 +5,7 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { Box } from '@/components'; import { Box, Text } from '@/components';
import ButtonCloseModal from '@/components/modal/ButtonCloseModal'; import ButtonCloseModal from '@/components/modal/ButtonCloseModal';
import { QuickSearch } from '@/components/quick-search'; import { QuickSearch } from '@/components/quick-search';
import { Doc, useDocUtils } from '@/docs/doc-management'; import { Doc, useDocUtils } from '@/docs/doc-management';
@@ -65,6 +65,7 @@ const DocSearchModalGlobal = ({
closeOnClickOutside closeOnClickOutside
size={isDesktop ? ModalSize.LARGE : ModalSize.FULL} size={isDesktop ? ModalSize.LARGE : ModalSize.FULL}
hideCloseButton hideCloseButton
aria-describedby="doc-search-modal-title"
> >
<Box <Box
aria-label={t('Search modal')} aria-label={t('Search modal')}
@@ -72,6 +73,14 @@ const DocSearchModalGlobal = ({
$justify="space-between" $justify="space-between"
className="--docs--doc-search-modal" className="--docs--doc-search-modal"
> >
<Text
as="h1"
$margin="0"
id="doc-search-modal-title"
className="sr-only"
>
{t('Search docs')}
</Text>
<Box $position="absolute" $css="top: 12px; right: 12px;"> <Box $position="absolute" $css="top: 12px; right: 12px;">
<ButtonCloseModal <ButtonCloseModal
aria-label={t('Close the search modal')} aria-label={t('Close the search modal')}

View File

@@ -135,12 +135,20 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
isOpen isOpen
closeOnClickOutside closeOnClickOutside
data-testid="doc-share-modal" data-testid="doc-share-modal"
aria-label={t('Share modal')} aria-describedby="doc-share-modal-title"
size={isDesktop ? ModalSize.LARGE : ModalSize.FULL} size={isDesktop ? ModalSize.LARGE : ModalSize.FULL}
onClose={onClose} onClose={onClose}
title={ title={
<Box $direction="row" $justify="space-between" $align="center"> <Box $direction="row" $justify="space-between" $align="center">
<Box $align="flex-start">{t('Share the document')}</Box> <Text
as="h1"
id="doc-share-modal-title"
$align="flex-start"
$size="small"
$weight="600"
>
{t('Share the document')}
</Text>
<ButtonCloseModal <ButtonCloseModal
aria-label={t('Close the share modal')} aria-label={t('Close the share modal')}
onClick={onClose} onClick={onClose}
@@ -199,6 +207,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
$textAlign="center" $textAlign="center"
$variation="600" $variation="600"
$size="sm" $size="sm"
as="p"
> >
{t( {t(
'You can view this document but need additional access to see its members or modify settings.', 'You can view this document but need additional access to see its members or modify settings.',

View File

@@ -69,10 +69,11 @@ export const ModalConfirmationVersion = ({
isOpen isOpen
closeOnClickOutside closeOnClickOutside
onClose={() => onClose()} onClose={() => onClose()}
aria-describedby="modal-confirmation-version-title"
rightActions={ rightActions={
<> <>
<Button <Button
aria-label={t('Close the modal')} aria-label={`${t('Cancel')} - ${t('Warning')}`}
color="secondary" color="secondary"
fullWidth fullWidth
onClick={() => onClose()} onClick={() => onClose()}
@@ -102,20 +103,24 @@ export const ModalConfirmationVersion = ({
} }
size={ModalSize.SMALL} size={ModalSize.SMALL}
title={ title={
<Text $size="h6" $align="flex-start" $variation="1000"> <Text
as="h1"
$margin="0"
id="modal-confirmation-version-title"
$size="h6"
$align="flex-start"
$variation="1000"
>
{t('Warning')} {t('Warning')}
</Text> </Text>
} }
> >
<Box <Box className="--docs--modal-confirmation-version">
aria-label={t('Modal confirmation to restore the version')}
className="--docs--modal-confirmation-version"
>
<Box> <Box>
<Text $variation="600"> <Text $variation="600" as="p">
{t('Your current document will revert to this version.')} {t('Your current document will revert to this version.')}
</Text> </Text>
<Text $variation="600"> <Text $variation="600" as="p">
{t('If a member is editing, his works can be lost.')} {t('If a member is editing, his works can be lost.')}
</Text> </Text>
</Box> </Box>

View File

@@ -48,6 +48,7 @@ export const ModalSelectVersion = ({
closeOnClickOutside={true} closeOnClickOutside={true}
size={ModalSize.EXTRA_LARGE} size={ModalSize.EXTRA_LARGE}
onClose={onClose} onClose={onClose}
aria-describedby="modal-select-version-title"
> >
<NoPaddingStyle /> <NoPaddingStyle />
<Box <Box
@@ -58,6 +59,14 @@ export const ModalSelectVersion = ({
$maxHeight="calc(100vh - 2em - 12px)" $maxHeight="calc(100vh - 2em - 12px)"
$overflow="hidden" $overflow="hidden"
> >
<Text
as="h1"
$margin="0"
id="modal-select-version-title"
className="sr-only"
>
{t('Version history')}
</Text>
<Box <Box
$css={css` $css={css`
display: flex; display: flex;

View File

@@ -272,6 +272,7 @@
"Close the search modal": "Such-Modal schließen", "Close the search modal": "Such-Modal schließen",
"Close the share modal": "Freigabe-Modal schließen", "Close the share modal": "Freigabe-Modal schließen",
"Close the version history modal": "Versionsverlauf-Modal schließen", "Close the version history modal": "Versionsverlauf-Modal schließen",
"Close the download modal": "Download-Modal schließen",
"Collaborate": "Zusammenarbeiten", "Collaborate": "Zusammenarbeiten",
"Collaborate and write in real time, without layout constraints.": "In Echtzeit und ohne Layout-Beschränkungen schreiben und 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.", "Collaborative writing, Simplified.": "Kollaboratives Schreiben, vereinfacht.",
@@ -463,6 +464,9 @@
"Close the search modal": "Close the search modal", "Close the search modal": "Close the search modal",
"Close the share modal": "Close the share modal", "Close the share modal": "Close the share modal",
"Close the version history modal": "Close the version history modal", "Close the version history modal": "Close the version history modal",
"Close the download modal": "Close the download modal",
"Cancel the deletion": "Cancel the deletion",
"Cancel the download": "Cancel the download",
"Open actions menu for document: {{title}}": "Open actions menu for document: {{title}}", "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}}", "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", "Share with {{count}} users_one": "Share with {{count}} user",
@@ -505,10 +509,13 @@
"Close the search modal": "Cerrar modal de búsqueda", "Close the search modal": "Cerrar modal de búsqueda",
"Close the share modal": "Cerrar modal de compartir", "Close the share modal": "Cerrar modal de compartir",
"Close the version history modal": "Cerrar modal de historial de versiones", "Close the version history modal": "Cerrar modal de historial de versiones",
"Close the download modal": "Cerrar modal de descarga",
"Collaborate": "Colabora", "Collaborate": "Colabora",
"Collaborate and write in real time, without layout constraints.": "Colaborar y escribir en tiempo real, sin restricciones de diseño.", "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.", "Collaborative writing, Simplified.": "Escritura colaborativa, más sencilla.",
"Confirm deletion": "Confirmar borrado", "Confirm deletion": "Confirmar borrado",
"Cancel the deletion": "Cancelar la eliminación",
"Cancel the download": "Cancelar la descarga",
"Connected": "Conectado", "Connected": "Conectado",
"Content modal to delete document": "Modal para eliminar el documento", "Content modal to delete document": "Modal para eliminar el documento",
"Content modal to export the document": "Ventana emergente para exportar el documento", "Content modal to export the document": "Ventana emergente para exportar el documento",
@@ -704,6 +711,8 @@
"Collaborative writing, Simplified.": "L'écriture collaborative simplifiée.", "Collaborative writing, Simplified.": "L'écriture collaborative simplifiée.",
"Confirm": "Confirmez", "Confirm": "Confirmez",
"Confirm deletion": "Confirmer la suppression", "Confirm deletion": "Confirmer la suppression",
"Cancel the deletion": "Annuler la suppression",
"Cancel the download": "Annuler le téléchargement",
"Confirmation button": "Bouton de confirmation", "Confirmation button": "Bouton de confirmation",
"Connected": "Connecté", "Connected": "Connecté",
"Content modal to delete document": "Contenu modal pour supprimer le document", "Content modal to delete document": "Contenu modal pour supprimer le document",
@@ -1088,10 +1097,13 @@
"Close the search modal": "Sluit het zoek venster", "Close the search modal": "Sluit het zoek venster",
"Close the share modal": "Sluit het delen venster", "Close the share modal": "Sluit het delen venster",
"Close the version history modal": "Sluit het versiehistorie venster", "Close the version history modal": "Sluit het versiehistorie venster",
"Close the download modal": "Sluit het download venster",
"Collaborate": "Samenwerken", "Collaborate": "Samenwerken",
"Collaborate and write in real time, without layout constraints.": "Samenwerken en schrijven in realtime, zonder lay-out beperkingen.", "Collaborate and write in real time, without layout constraints.": "Samenwerken en schrijven in realtime, zonder lay-out beperkingen.",
"Collaborative writing, Simplified.": "Vereenvoudigd samenwerkend", "Collaborative writing, Simplified.": "Vereenvoudigd samenwerkend",
"Confirm deletion": "Bevestig verwijdering", "Confirm deletion": "Bevestig verwijdering",
"Cancel the deletion": "Annuleer de verwijdering",
"Cancel the download": "Annuleer de download",
"Connected": "Verbonden", "Connected": "Verbonden",
"Content modal to delete document": "Content venster om het document te verwijderen", "Content modal to delete document": "Content venster om het document te verwijderen",
"Content modal to export the document": "Content venster om document te exporteren", "Content modal to export the document": "Content venster om document te exporteren",

View File

@@ -82,3 +82,16 @@ main ::-webkit-scrollbar-thumb:hover,
nextjs-portal { nextjs-portal {
display: none; display: none;
} }
/* Screen reader only - visually hidden but accessible to screen readers */
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip-path: inset(50%) !important;
white-space: nowrap !important;
border: 0 !important;
}