️(frontend) improve left panel accessibility (#1262)

Improve overall accessibility of the left panel:
- ️(frontend) make LeftPanelTargetFilter accessible and use Box as nav
- ️(frontend) improve accessibility in left panel components
- (e2e) fix e2e test to expect aria-current instead of aria-selected
- (frontend) add semantic ul/li to LeftPanel
- (frontend) improve favorite item a11y and update e2e test accordingly
This commit is contained in:
Cyril G
2025-08-06 14:20:53 +02:00
committed by GitHub
parent 409e073192
commit afbacb0a24
10 changed files with 100 additions and 37 deletions

View File

@@ -13,6 +13,8 @@ and this project adheres to
- ⚡️(frontend) improve accessibility:
- #1248
- #1235
- #1255
- #1262
## [3.5.0] - 2025-07-31
@@ -31,9 +33,7 @@ and this project adheres to
- ♻️(frontend) redirect to doc after duplicate #1175
- 🔧(project) change env.d system by using local files #1200
- ⚡️(frontend) improve tree stability #1207
- ⚡️(frontend) improve accessibility
- #1232
- #1255
- ⚡️(frontend) improve accessibility #1232
- 🛂(frontend) block drag n drop when not desktop #1239
### Fixed

View File

@@ -119,7 +119,7 @@ test.describe('Document grid item options', () => {
await page.getByText('push_pin').click();
// Check is pinned
await expect(row.getByLabel('Pin document icon')).toBeVisible();
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
@@ -128,7 +128,7 @@ test.describe('Document grid item options', () => {
await page.getByText('Unpin').click();
// Check is unpinned
await expect(row.getByLabel('Pin document icon')).toBeHidden();
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
});
@@ -227,18 +227,18 @@ test.describe('Documents filters', () => {
// Initial state
await expect(allDocs).toBeVisible();
await expect(allDocs).toHaveAttribute('aria-selected', 'true');
await expect(allDocs).toHaveAttribute('aria-current', 'page');
await expect(myDocs).toBeVisible();
await expect(myDocs).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
await expect(myDocs).toHaveAttribute('aria-selected', 'false');
await expect(myDocs).not.toHaveAttribute('aria-current');
await expect(sharedWithMe).toBeVisible();
await expect(sharedWithMe).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)',
);
await expect(sharedWithMe).toHaveAttribute('aria-selected', 'false');
await expect(sharedWithMe).not.toHaveAttribute('aria-current');
await allDocs.click();

View File

@@ -409,7 +409,7 @@ test.describe('Doc Header', () => {
const row = await getGridRow(page, docTitle);
// Check is pinned
await expect(row.getByLabel('Pin document icon')).toBeVisible();
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
@@ -424,7 +424,7 @@ test.describe('Doc Header', () => {
await page.goto('/');
// Check is unpinned
await expect(row.getByLabel('Pin document icon')).toBeHidden();
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
});

View File

@@ -33,6 +33,13 @@ const StyledButton = styled(Button)<StyledButtonProps>`
font-size: 0.938rem;
padding: 0;
${({ $css }) => $css};
&:focus-visible {
outline: 2px solid var(--c--theme--colors--primary-500);
outline-offset: 2px;
border-radius: 4px;
transition: none;
}
`;
export interface DropButtonProps {

View File

@@ -52,14 +52,17 @@ export const SimpleDocItem = ({
filter: drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.05));
`}
$padding={`${spacingsTokens['3xs']} 0`}
data-testid={isPinned ? `doc-pinned-${doc.id}` : undefined}
>
{isPinned ? (
<PinnedDocumentIcon
aria-hidden="true"
aria-label={t('Pin document icon')}
color={colorsTokens['primary-500']}
/>
) : (
<SimpleFileIcon
aria-hidden="true"
aria-label={t('Simple document icon')}
color={colorsTokens['primary-500']}
/>

View File

@@ -1,5 +1,6 @@
import { useModal } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { DropdownMenu, DropdownMenuOption, Icon } from '@/components';
import {
@@ -83,6 +84,13 @@ export const DocsGridActions = ({
iconName="more_horiz"
$theme="primary"
$variation="600"
aria-label={t('More options')}
$css={css`
cursor: pointer;
&:hover {
opacity: 0.8;
}
`}
/>
</DropdownMenu>

View File

@@ -1,26 +1,24 @@
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { usePathname, useSearchParams } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, BoxButton, Icon, Text } from '@/components';
import { Box, Icon, StyledLink, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { DocDefaultFilter } from '@/docs/doc-management';
import { useLeftPanelStore } from '@/features/left-panel';
export const LeftPanelTargetFilters = () => {
const { t } = useTranslation();
const pathname = usePathname();
const searchParams = useSearchParams();
const { togglePanel } = useLeftPanelStore();
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
const searchParams = useSearchParams();
const target =
(searchParams.get('target') as DocDefaultFilter) ??
DocDefaultFilter.ALL_DOCS;
const router = useRouter();
const defaultQueries = [
{
icon: 'apps',
@@ -39,15 +37,20 @@ export const LeftPanelTargetFilters = () => {
},
];
const onSelectQuery = (query: DocDefaultFilter) => {
const buildHref = (query: DocDefaultFilter) => {
const params = new URLSearchParams(searchParams);
params.set('target', query);
router.push(`${pathname}?${params.toString()}`);
return `${pathname}?${params.toString()}`;
};
const handleClick = () => {
togglePanel();
};
return (
<Box
as="nav"
aria-label={t('Document sections')}
$justify="center"
$padding={{ horizontal: 'sm' }}
$gap={spacingsTokens['2xs']}
@@ -55,28 +58,36 @@ export const LeftPanelTargetFilters = () => {
>
{defaultQueries.map((query) => {
const isActive = target === query.targetQuery;
const href = buildHref(query.targetQuery);
return (
<BoxButton
aria-label={query.label}
<StyledLink
key={query.label}
onClick={() => onSelectQuery(query.targetQuery)}
$direction="row"
aria-selected={isActive}
$align="center"
$justify="flex-start"
$gap={spacingsTokens['xs']}
$radius={spacingsTokens['3xs']}
$padding={{ all: '2xs' }}
href={href}
aria-label={query.label}
aria-current={isActive ? 'page' : undefined}
onClick={handleClick}
$css={css`
cursor: pointer;
display: flex;
align-items: center;
justify-content: flex-start;
gap: ${spacingsTokens['xs']};
padding: ${spacingsTokens['2xs']};
border-radius: ${spacingsTokens['3xs']};
background-color: ${isActive
? colorsTokens['greyscale-100']
: undefined};
font-weight: ${isActive ? 700 : undefined};
: 'transparent'};
font-weight: ${isActive ? 700 : 400};
color: inherit;
text-decoration: none;
cursor: pointer;
&:hover {
background-color: ${colorsTokens['greyscale-100']};
}
&:focus-visible {
outline: 2px solid ${colorsTokens['primary-500']};
outline-offset: 2px;
}
`}
>
<Icon
@@ -86,7 +97,7 @@ export const LeftPanelTargetFilters = () => {
<Text $variation={isActive ? '1000' : '700'} $size="sm">
{query.label}
</Text>
</BoxButton>
</StyledLink>
);
})}
</Box>

View File

@@ -1,4 +1,6 @@
import { useModal } from '@openfun/cunningham-react';
import { t } from 'i18next';
import { DateTime } from 'luxon';
import { css } from 'styled-components';
import { Box, StyledLink } from '@/components';
@@ -14,11 +16,12 @@ type LeftPanelFavoriteItemProps = {
export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => {
const shareModal = useModal();
const { spacingsTokens } = useCunninghamTheme();
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
const { isDesktop } = useResponsiveStore();
return (
<Box
as="li"
$direction="row"
$align="center"
$justify="space-between"
@@ -28,7 +31,8 @@ export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => {
.pinned-actions {
opacity: ${isDesktop ? 0 : 1};
}
&:hover {
&:hover,
&:focus-within {
cursor: pointer;
background-color: var(--c--theme--colors--greyscale-100);
@@ -36,11 +40,20 @@ export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => {
opacity: 1;
}
}
&:focus-visible {
outline: 2px solid ${colorsTokens['primary-500']};
outline-offset: 2px;
border-radius: ${spacingsTokens['3xs']};
}
`}
key={doc.id}
className="--docs--left-panel-favorite-item"
>
<StyledLink href={`/docs/${doc.id}`} $css="overflow: auto;">
<StyledLink
href={`/docs/${doc.id}`}
$css="overflow: auto;"
aria-label={`${doc.title}, ${t('Updated')} ${DateTime.fromISO(doc.updated_at).toRelative()}`}
>
<SimpleDocItem showAccesses doc={doc} />
</StyledLink>
<div className="pinned-actions">

View File

@@ -23,7 +23,11 @@ export const LeftPanelFavorites = () => {
}
return (
<Box className="--docs--left-panel-favorites">
<Box
as="nav"
aria-label={t('Pinned documents')}
className="--docs--left-panel-favorites"
>
<HorizontalSeparator $withPadding={false} />
<Box
$justify="center"
@@ -41,6 +45,7 @@ export const LeftPanelFavorites = () => {
{t('Pinned documents')}
</Text>
<InfiniteScroll
as="ul"
hasMore={docs.hasNextPage}
isLoading={docs.isFetchingNextPage}
next={() => void docs.fetchNextPage()}

View File

@@ -43,6 +43,7 @@
"Document accessible to any connected person": "Restr a c'hall bezañ tizhet gant ne vern piv a vefe kevreet",
"Document duplicated successfully!": "Restr eilet gant berzh!",
"Document owner": "Perc'henn ar restr",
"Document sections": "Rannoù an teul",
"Docx": "Docx",
"Download": "Pellgargañ",
"Download anyway": "Pellgargañ memestra",
@@ -216,6 +217,7 @@
"Document accessible to any connected person": "Dokument für jeden angemeldeten Benutzer zugänglich",
"Document duplicated successfully!": "Dokument erfolgreich dupliziert!",
"Document owner": "Besitzer des Dokuments",
"Document sections": "Dokumentabschnitte",
"Docx": "Docx",
"Download": "Herunterladen",
"Download anyway": "Trotzdem herunterladen",
@@ -264,6 +266,7 @@
"Modal confirmation to download the attachment": "Modale Bestätigung zum Herunterladen des Anhangs",
"Modal confirmation to restore the version": "Modale Bestätigung um die Version wiederherzustellen",
"More docs": "Weitere Dokumente",
"More options": "Weitere Optionen",
"Move": "Verschieben",
"Move document": "Dokument verschieben",
"Move to my docs": "In \"Meine Dokumente\" verschieben",
@@ -344,6 +347,7 @@
"Unnamed document": "Unbenanntes Dokument",
"Unpin": "Lösen",
"Untitled document": "Unbenanntes Dokument",
"Updated": "Aktualisiert",
"Updated at": "Aktualisiert am",
"Use as prompt": "Als Prompt verwenden",
"Version history": "Versionsverlauf",
@@ -367,10 +371,13 @@
"en": {
"translation": {
"Search docs": "Search docs",
"More options": "More options",
"Pinned documents": "Pinned documents",
"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",
"Shared with {{count}} users_other": "Shared with {{count}} users"
"Shared with {{count}} users_other": "Shared with {{count}} users",
"Updated": "Updated"
}
},
"es": {
@@ -429,6 +436,7 @@
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs: su nuevo compañero para colaborar en documentos de forma eficiente, intuitiva y segura.",
"Document accessible to any connected person": "Documento accesible a cualquier persona conectada",
"Document owner": "Propietario del documento",
"Document sections": "Secciones del documento",
"Docx": "Docx",
"Download": "Descargar",
"Download anyway": "Descargar de todos modos",
@@ -470,6 +478,7 @@
"Modal confirmation to download the attachment": "Modal de confirmación para descargar el archivo adjunto",
"Modal confirmation to restore the version": "Modal de confirmación para restaurar la versión",
"More docs": "Más documentos",
"More options": "Más opciones",
"My docs": "Mis documentos",
"Name": "Nombre",
"New doc": "Nuevo documento",
@@ -539,6 +548,7 @@
"Type the name of a document": "Escribe el nombre de un documento",
"Unpin": "Desanclar",
"Untitled document": "Documento sin título",
"Updated": "Actualizado",
"Updated at": "Actualizado a las",
"Use as prompt": "Usar como prompt",
"Version history": "Historial de versiones",
@@ -622,6 +632,7 @@
"Document accessible to any connected person": "Document accessible à toute personne connectée",
"Document duplicated successfully!": "Document dupliqué avec succès !",
"Document owner": "Propriétaire du document",
"Document sections": "Sections des documents",
"Docx": "Docx",
"Download": "Télécharger",
"Download anyway": "Télécharger malgré tout",
@@ -679,6 +690,7 @@
"Modal confirmation to download the attachment": "Modale de confirmation pour télécharger la pièce jointe",
"Modal confirmation to restore the version": "Modale de confirmation pour restaurer la version",
"More docs": "Plus de documents",
"More options": "Plus d'options",
"Move": "Déplacer",
"Move document": "Déplacer le document",
"Move to my docs": "Déplacer vers mes docs",
@@ -763,6 +775,7 @@
"Type a name or email": "Tapez un nom ou un email",
"Type the name of a document": "Tapez le nom d'un document",
"Unnamed document": "Document sans titre",
"Updated": "Mise à jour",
"Unpin": "Désépingler",
"Untitled document": "Document sans titre",
"Updated at": "Mise à jour le",
@@ -988,6 +1001,7 @@
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs: Je nieuwe metgezel om efficiënt, intuïtief en veilig samen te werken aan documenten.",
"Document accessible to any connected person": "Document is toegankelijk voor ieder verbonden persoon",
"Document owner": "Document eigenaar",
"Document sections": "Document secties",
"Docx": "Docx",
"Download": "Download",
"Download anyway": "Download alsnog",
@@ -1028,6 +1042,7 @@
"Modal confirmation to download the attachment": "Venster bevestiging om bijlage te downloaden",
"Modal confirmation to restore the version": "Bevestiging modal om de versie te herstellen",
"More docs": "Meer documenten",
"More options": "Meer opties",
"My docs": "Mijn documenten",
"Name": "Naam",
"New doc": "Nieuw document",
@@ -1095,6 +1110,7 @@
"Type the name of a document": "Vul de naam van een document in",
"Unpin": "Losmaken",
"Untitled document": "Naamloos document",
"Updated": "Bijgewerkt",
"Updated at": "Geüpdate op",
"Use as prompt": "Gebruik als prompt",
"Version history": "Versie historie",