Skip to content

Commit e85e7d6

Browse files
committed
♿️(frontend) fine grained accessibility for right panel
- improve semantics and aria attributes - gives the focus to the panel when open or switching panels - gives back the focus to the trigger element when closing the panel
1 parent 981aef0 commit e85e7d6

6 files changed

Lines changed: 107 additions & 14 deletions

File tree

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,4 +547,47 @@ test.describe('Doc Comments Side Panel', () => {
547547
'bn-thread-mark-selected',
548548
);
549549
});
550+
551+
test('it checks comments accessibility', async ({ page, browserName }) => {
552+
await createDoc(page, 'comment-doc-panel', browserName, 1);
553+
554+
// Create comment thread
555+
const editor = await writeInEditor({ page, text: 'Hello World' });
556+
await editor.getByText('Hello').selectText();
557+
await page.getByRole('button', { name: 'Add comment' }).click();
558+
559+
const thread = page.locator('.bn-thread');
560+
await thread.getByRole('paragraph').first().fill('This is a comment');
561+
await thread.locator('[data-test="save"]').click();
562+
563+
// Open comment side panel and check aria attributes
564+
await page
565+
.getByRole('button', { name: 'Show the comments sidebar' })
566+
.click();
567+
568+
const elCommentsSidePanel = page.getByLabel('Comments side panel');
569+
await expect(elCommentsSidePanel).not.toHaveAttribute('inert');
570+
571+
// Check panel get the focus when opening
572+
await page.keyboard.press('Tab');
573+
await expect(
574+
elCommentsSidePanel.getByRole('button', { name: 'Filter comments' }),
575+
).toBeFocused();
576+
await page.keyboard.press('Tab');
577+
578+
// Check the focus goes back to the button that open the side panel
579+
await expect(
580+
elCommentsSidePanel.getByRole('button', {
581+
name: 'Close the comments sidebar',
582+
}),
583+
).toBeFocused();
584+
await page.keyboard.press('Enter');
585+
await expect(elCommentsSidePanel).toBeHidden();
586+
await expect(
587+
page.getByRole('complementary', { name: 'Side panel' }),
588+
).toHaveAttribute('inert');
589+
await expect(
590+
page.getByRole('button', { name: 'Show the comments sidebar' }),
591+
).toBeFocused();
592+
});
550593
});

src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/CommentSideBar.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { Button, Tooltip } from '@gouvfr-lasuite/cunningham-react';
1+
import {
2+
Button,
3+
ButtonElement,
4+
Tooltip,
5+
} from '@gouvfr-lasuite/cunningham-react';
26
import { DropdownMenu } from '@gouvfr-lasuite/ui-kit';
37
import { useEffect, useRef, useState } from 'react';
48
import { useTranslation } from 'react-i18next';
@@ -9,6 +13,7 @@ import SortingResolvedSVG from '@/assets/icons/ui-kit/filter-notification.svg';
913
import SortingOpenSVG from '@/assets/icons/ui-kit/filter_list.svg';
1014
import { Box, ButtonCloseModal, Text } from '@/components/';
1115
import { useRightPanelStore } from '@/features/right-panel/stores/useRightPanelStore';
16+
import { useFocusStore } from '@/stores';
1217

1318
import { useCommentSidebarStore } from './useCommentSidebarStore';
1419

@@ -43,7 +48,9 @@ export const CommentSideBar = ({ onClose }: CommentSideBarProps) => {
4348
>
4449
<Box $direction="row" $align="center" $justify="space-between">
4550
<Box $direction="row" $align="center" $gap="2xs">
46-
<Text $weight="bold">{t('Comments')}</Text>
51+
<Text as="h2" $weight="bold" $size="16px" $margin="0">
52+
{t('Comments')}
53+
</Text>
4754

4855
<DropdownMenu
4956
options={[
@@ -88,7 +95,6 @@ export const CommentSideBar = ({ onClose }: CommentSideBarProps) => {
8895
e.preventDefault();
8996
setOpen((o) => !o);
9097
}}
91-
tabIndex={-1}
9298
/>
9399
</Tooltip>
94100
</DropdownMenu>
@@ -112,6 +118,8 @@ export const CommentSideBarButton = () => {
112118
const { t } = useTranslation();
113119
const { isPanelOpen, activePanel, setActivePanel, setIsPanelOpen } =
114120
useRightPanelStore();
121+
const buttonRef = useRef<ButtonElement>(null);
122+
const { addLastFocus } = useFocusStore();
115123

116124
const isActive = isPanelOpen && activePanel === 'comments';
117125
const ariaLabel = isActive
@@ -120,12 +128,14 @@ export const CommentSideBarButton = () => {
120128

121129
return (
122130
<Button
131+
ref={buttonRef}
123132
size="small"
124133
onClick={() => {
125134
if (isActive) {
126135
setIsPanelOpen(false);
127136
} else {
128137
setActivePanel('comments');
138+
addLastFocus(buttonRef.current);
129139
}
130140
}}
131141
aria-label={ariaLabel}

src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/styles.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,13 @@ export const DocsCommentsStyle = createGlobalStyle<{
310310
min-height: 0;
311311
overflow: auto;
312312
313+
.bn-editor[contenteditable="false"]{
314+
&:focus-visible {
315+
outline: 2px solid var(--c--globals--colors--brand-400);
316+
outline-offset: -2px;
317+
}
318+
}
319+
313320
.bn-threads-sidebar {
314321
gap: 0;
315322
border-radius: 0;

src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContentSideBar.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Button } from '@gouvfr-lasuite/cunningham-react';
2-
import { useEffect, useState } from 'react';
1+
import { Button, ButtonElement } from '@gouvfr-lasuite/cunningham-react';
2+
import { useEffect, useRef, useState } from 'react';
33
import { useTranslation } from 'react-i18next';
44
import { css } from 'styled-components';
55

@@ -10,6 +10,7 @@ import { useEditorStore } from '@/docs/doc-editor/stores/useEditorStore';
1010
import { useHeadingStore } from '@/docs/doc-editor/stores/useHeadingStore';
1111
import { useRightPanelStore } from '@/features/right-panel/stores/useRightPanelStore';
1212
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
13+
import { useFocusStore } from '@/stores/useFocusStore';
1314

1415
import { Heading } from './Heading';
1516

@@ -94,7 +95,9 @@ export const TableContentSideBar = ({ onClose }: TableContentSideBarProps) => {
9495
`}
9596
>
9697
<Box $direction="row" $align="center" $justify="space-between">
97-
<Text $weight="bold">{t('Table of Contents')}</Text>
98+
<Text as="h2" $weight="bold" $size="16px" $margin="0">
99+
{t('Table of Contents')}
100+
</Text>
98101
<ButtonCloseModal
99102
aria-label={t('Close the table of contents sidebar')}
100103
onClick={onClose}
@@ -140,6 +143,8 @@ export const TableContentSideBarButton = () => {
140143
const { t } = useTranslation();
141144
const { isPanelOpen, activePanel, setActivePanel, setIsPanelOpen } =
142145
useRightPanelStore();
146+
const buttonRef = useRef<ButtonElement>(null);
147+
const { addLastFocus } = useFocusStore();
143148

144149
const isActive = isPanelOpen && activePanel === 'tableContent';
145150
const ariaLabel = isActive
@@ -148,12 +153,14 @@ export const TableContentSideBarButton = () => {
148153

149154
return (
150155
<Button
156+
ref={buttonRef}
151157
size="small"
152158
onClick={() => {
153159
if (isActive) {
154160
setIsPanelOpen(false);
155161
} else {
156162
setActivePanel('tableContent');
163+
addLastFocus(buttonRef.current);
157164
}
158165
}}
159166
aria-label={ariaLabel}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ export const ResizableLeftPanel = ({
148148
<Panel
149149
ref={ref}
150150
className="--docs--resizable-left-panel"
151+
inert={!isPanelOpen}
151152
collapsible={!isPanelOpen}
152153
collapsedSize={0}
153154
style={{

src/frontend/apps/impress/src/features/right-panel/components/RightPanel.tsx

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from 'react';
1+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { css } from 'styled-components';
44

@@ -7,7 +7,7 @@ import { CommentSideBar } from '@/features/docs/doc-editor/components/comments/C
77
import { useDocStore, useProviderStore } from '@/features/docs/doc-management';
88
import { TableContentSideBar } from '@/features/docs/doc-table-content/components/TableContentSideBar';
99
import { HEADER_HEIGHT } from '@/features/header';
10-
import { useResponsiveStore } from '@/stores';
10+
import { useFocusStore, useResponsiveStore } from '@/stores';
1111

1212
import {
1313
RightPanelView,
@@ -22,6 +22,7 @@ export const RightPanel = () => {
2222
const { provider, isReady } = useProviderStore();
2323
const isProviderReady =
2424
isReady && provider && provider?.configuration.name === doc?.id;
25+
const { restoreFocus } = useFocusStore();
2526

2627
/**
2728
* Keep rendering the last active panel during the close animation,
@@ -40,21 +41,41 @@ export const RightPanel = () => {
4041
}
4142
}, [activePanel]);
4243

44+
/**
45+
* Focus the panel when it opens or when the rendered panel changes,
46+
* so that keyboard users are placed in the panel content immediately.
47+
*/
48+
const panelRef = useRef<HTMLElement>(null);
49+
useLayoutEffect(() => {
50+
if (isPanelOpen) {
51+
panelRef.current?.focus();
52+
}
53+
}, [isPanelOpen, renderedPanel]);
54+
4355
if (!doc || !isProviderReady) {
4456
return null;
4557
}
4658

47-
const ariaLabel = isPanelOpen
48-
? t('Right panel, currently open')
49-
: t('Right panel, currently closed');
59+
const panelLabel =
60+
renderedPanel === 'comments'
61+
? t('Comments side panel')
62+
: renderedPanel === 'tableContent'
63+
? t('Table of contents side panel')
64+
: t('Side panel');
5065

51-
const handleClose = () => setIsPanelOpen(false);
66+
const handleClose = () => {
67+
setIsPanelOpen(false);
68+
restoreFocus();
69+
};
5270

5371
return (
5472
<Box
73+
as="aside"
74+
ref={panelRef}
75+
tabIndex={-1}
5576
className="--docs--right-panel"
56-
aria-label={ariaLabel}
57-
aria-expanded={isPanelOpen}
77+
aria-label={panelLabel}
78+
inert={!isPanelOpen}
5879
$width="300px"
5980
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
6081
$position={isMobile ? 'absolute' : 'sticky'}
@@ -76,6 +97,10 @@ export const RightPanel = () => {
7697
margin-left: 0rem;
7798
width: 0;
7899
`}
100+
101+
&:focus {
102+
outline: none;
103+
}
79104
`}
80105
>
81106
{renderedPanel === 'tableContent' && (

0 commit comments

Comments
 (0)