Skip to content

Commit afbacb0

Browse files
authored
♿️(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
1 parent 409e073 commit afbacb0

File tree

10 files changed

+100
-37
lines changed

10 files changed

+100
-37
lines changed

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ and this project adheres to
1313
- ⚡️(frontend) improve accessibility:
1414
- #1248
1515
- #1235
16+
- #1255
17+
- #1262
1618

1719
## [3.5.0] - 2025-07-31
1820

@@ -31,9 +33,7 @@ and this project adheres to
3133
- ♻️(frontend) redirect to doc after duplicate #1175
3234
- 🔧(project) change env.d system by using local files #1200
3335
- ⚡️(frontend) improve tree stability #1207
34-
- ⚡️(frontend) improve accessibility
35-
- #1232
36-
- #1255
36+
- ⚡️(frontend) improve accessibility #1232
3737
- 🛂(frontend) block drag n drop when not desktop #1239
3838

3939
### Fixed

src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ test.describe('Document grid item options', () => {
119119
await page.getByText('push_pin').click();
120120

121121
// Check is pinned
122-
await expect(row.getByLabel('Pin document icon')).toBeVisible();
122+
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
123123
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
124124
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
125125

@@ -128,7 +128,7 @@ test.describe('Document grid item options', () => {
128128
await page.getByText('Unpin').click();
129129

130130
// Check is unpinned
131-
await expect(row.getByLabel('Pin document icon')).toBeHidden();
131+
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
132132
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
133133
});
134134

@@ -227,18 +227,18 @@ test.describe('Documents filters', () => {
227227

228228
// Initial state
229229
await expect(allDocs).toBeVisible();
230-
await expect(allDocs).toHaveAttribute('aria-selected', 'true');
230+
await expect(allDocs).toHaveAttribute('aria-current', 'page');
231231

232232
await expect(myDocs).toBeVisible();
233233
await expect(myDocs).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
234-
await expect(myDocs).toHaveAttribute('aria-selected', 'false');
234+
await expect(myDocs).not.toHaveAttribute('aria-current');
235235

236236
await expect(sharedWithMe).toBeVisible();
237237
await expect(sharedWithMe).toHaveCSS(
238238
'background-color',
239239
'rgba(0, 0, 0, 0)',
240240
);
241-
await expect(sharedWithMe).toHaveAttribute('aria-selected', 'false');
241+
await expect(sharedWithMe).not.toHaveAttribute('aria-current');
242242

243243
await allDocs.click();
244244

src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ test.describe('Doc Header', () => {
409409
const row = await getGridRow(page, docTitle);
410410

411411
// Check is pinned
412-
await expect(row.getByLabel('Pin document icon')).toBeVisible();
412+
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
413413
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
414414
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
415415

@@ -424,7 +424,7 @@ test.describe('Doc Header', () => {
424424
await page.goto('/');
425425

426426
// Check is unpinned
427-
await expect(row.getByLabel('Pin document icon')).toBeHidden();
427+
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
428428
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
429429
});
430430

src/frontend/apps/impress/src/components/DropButton.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ const StyledButton = styled(Button)<StyledButtonProps>`
3333
font-size: 0.938rem;
3434
padding: 0;
3535
${({ $css }) => $css};
36+
37+
&:focus-visible {
38+
outline: 2px solid var(--c--theme--colors--primary-500);
39+
outline-offset: 2px;
40+
border-radius: 4px;
41+
transition: none;
42+
}
3643
`;
3744

3845
export interface DropButtonProps {

src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,17 @@ export const SimpleDocItem = ({
5252
filter: drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.05));
5353
`}
5454
$padding={`${spacingsTokens['3xs']} 0`}
55+
data-testid={isPinned ? `doc-pinned-${doc.id}` : undefined}
5556
>
5657
{isPinned ? (
5758
<PinnedDocumentIcon
59+
aria-hidden="true"
5860
aria-label={t('Pin document icon')}
5961
color={colorsTokens['primary-500']}
6062
/>
6163
) : (
6264
<SimpleFileIcon
65+
aria-hidden="true"
6366
aria-label={t('Simple document icon')}
6467
color={colorsTokens['primary-500']}
6568
/>

src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useModal } from '@openfun/cunningham-react';
22
import { useTranslation } from 'react-i18next';
3+
import { css } from 'styled-components';
34

45
import { DropdownMenu, DropdownMenuOption, Icon } from '@/components';
56
import {
@@ -83,6 +84,13 @@ export const DocsGridActions = ({
8384
iconName="more_horiz"
8485
$theme="primary"
8586
$variation="600"
87+
aria-label={t('More options')}
88+
$css={css`
89+
cursor: pointer;
90+
&:hover {
91+
opacity: 0.8;
92+
}
93+
`}
8694
/>
8795
</DropdownMenu>
8896

src/frontend/apps/impress/src/features/left-panel/components/LefPanelTargetFilters.tsx

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
1-
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
1+
import { usePathname, useSearchParams } from 'next/navigation';
22
import { useTranslation } from 'react-i18next';
33
import { css } from 'styled-components';
44

5-
import { Box, BoxButton, Icon, Text } from '@/components';
5+
import { Box, Icon, StyledLink, Text } from '@/components';
66
import { useCunninghamTheme } from '@/cunningham';
77
import { DocDefaultFilter } from '@/docs/doc-management';
88
import { useLeftPanelStore } from '@/features/left-panel';
99

1010
export const LeftPanelTargetFilters = () => {
1111
const { t } = useTranslation();
12-
1312
const pathname = usePathname();
13+
const searchParams = useSearchParams();
14+
1415
const { togglePanel } = useLeftPanelStore();
1516
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
1617

17-
const searchParams = useSearchParams();
1818
const target =
1919
(searchParams.get('target') as DocDefaultFilter) ??
2020
DocDefaultFilter.ALL_DOCS;
2121

22-
const router = useRouter();
23-
2422
const defaultQueries = [
2523
{
2624
icon: 'apps',
@@ -39,44 +37,57 @@ export const LeftPanelTargetFilters = () => {
3937
},
4038
];
4139

42-
const onSelectQuery = (query: DocDefaultFilter) => {
40+
const buildHref = (query: DocDefaultFilter) => {
4341
const params = new URLSearchParams(searchParams);
4442
params.set('target', query);
45-
router.push(`${pathname}?${params.toString()}`);
43+
return `${pathname}?${params.toString()}`;
44+
};
45+
46+
const handleClick = () => {
4647
togglePanel();
4748
};
4849

4950
return (
5051
<Box
52+
as="nav"
53+
aria-label={t('Document sections')}
5154
$justify="center"
5255
$padding={{ horizontal: 'sm' }}
5356
$gap={spacingsTokens['2xs']}
5457
className="--docs--left-panel-target-filters"
5558
>
5659
{defaultQueries.map((query) => {
5760
const isActive = target === query.targetQuery;
61+
const href = buildHref(query.targetQuery);
5862

5963
return (
60-
<BoxButton
61-
aria-label={query.label}
64+
<StyledLink
6265
key={query.label}
63-
onClick={() => onSelectQuery(query.targetQuery)}
64-
$direction="row"
65-
aria-selected={isActive}
66-
$align="center"
67-
$justify="flex-start"
68-
$gap={spacingsTokens['xs']}
69-
$radius={spacingsTokens['3xs']}
70-
$padding={{ all: '2xs' }}
66+
href={href}
67+
aria-label={query.label}
68+
aria-current={isActive ? 'page' : undefined}
69+
onClick={handleClick}
7170
$css={css`
72-
cursor: pointer;
71+
display: flex;
72+
align-items: center;
73+
justify-content: flex-start;
74+
gap: ${spacingsTokens['xs']};
75+
padding: ${spacingsTokens['2xs']};
76+
border-radius: ${spacingsTokens['3xs']};
7377
background-color: ${isActive
7478
? colorsTokens['greyscale-100']
75-
: undefined};
76-
font-weight: ${isActive ? 700 : undefined};
79+
: 'transparent'};
80+
font-weight: ${isActive ? 700 : 400};
81+
color: inherit;
82+
text-decoration: none;
83+
cursor: pointer;
7784
&:hover {
7885
background-color: ${colorsTokens['greyscale-100']};
7986
}
87+
&:focus-visible {
88+
outline: 2px solid ${colorsTokens['primary-500']};
89+
outline-offset: 2px;
90+
}
8091
`}
8192
>
8293
<Icon
@@ -86,7 +97,7 @@ export const LeftPanelTargetFilters = () => {
8697
<Text $variation={isActive ? '1000' : '700'} $size="sm">
8798
{query.label}
8899
</Text>
89-
</BoxButton>
100+
</StyledLink>
90101
);
91102
})}
92103
</Box>

src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavoriteItem.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { useModal } from '@openfun/cunningham-react';
2+
import { t } from 'i18next';
3+
import { DateTime } from 'luxon';
24
import { css } from 'styled-components';
35

46
import { Box, StyledLink } from '@/components';
@@ -14,11 +16,12 @@ type LeftPanelFavoriteItemProps = {
1416

1517
export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => {
1618
const shareModal = useModal();
17-
const { spacingsTokens } = useCunninghamTheme();
19+
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
1820
const { isDesktop } = useResponsiveStore();
1921

2022
return (
2123
<Box
24+
as="li"
2225
$direction="row"
2326
$align="center"
2427
$justify="space-between"
@@ -28,19 +31,29 @@ export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => {
2831
.pinned-actions {
2932
opacity: ${isDesktop ? 0 : 1};
3033
}
31-
&:hover {
34+
&:hover,
35+
&:focus-within {
3236
cursor: pointer;
3337
3438
background-color: var(--c--theme--colors--greyscale-100);
3539
.pinned-actions {
3640
opacity: 1;
3741
}
3842
}
43+
&:focus-visible {
44+
outline: 2px solid ${colorsTokens['primary-500']};
45+
outline-offset: 2px;
46+
border-radius: ${spacingsTokens['3xs']};
47+
}
3948
`}
4049
key={doc.id}
4150
className="--docs--left-panel-favorite-item"
4251
>
43-
<StyledLink href={`/docs/${doc.id}`} $css="overflow: auto;">
52+
<StyledLink
53+
href={`/docs/${doc.id}`}
54+
$css="overflow: auto;"
55+
aria-label={`${doc.title}, ${t('Updated')} ${DateTime.fromISO(doc.updated_at).toRelative()}`}
56+
>
4457
<SimpleDocItem showAccesses doc={doc} />
4558
</StyledLink>
4659
<div className="pinned-actions">

src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavorites.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ export const LeftPanelFavorites = () => {
2323
}
2424

2525
return (
26-
<Box className="--docs--left-panel-favorites">
26+
<Box
27+
as="nav"
28+
aria-label={t('Pinned documents')}
29+
className="--docs--left-panel-favorites"
30+
>
2731
<HorizontalSeparator $withPadding={false} />
2832
<Box
2933
$justify="center"
@@ -41,6 +45,7 @@ export const LeftPanelFavorites = () => {
4145
{t('Pinned documents')}
4246
</Text>
4347
<InfiniteScroll
48+
as="ul"
4449
hasMore={docs.hasNextPage}
4550
isLoading={docs.isFetchingNextPage}
4651
next={() => void docs.fetchNextPage()}

0 commit comments

Comments
 (0)