Skip to content

Commit ae9b169

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 3a28843 commit ae9b169

3 files changed

Lines changed: 56 additions & 14 deletions

File tree

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-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, useHeadingStore } from '@/docs/doc-editor';
1010
import { HEADER_HEIGHT } from '@/features/header';
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

@@ -92,7 +93,9 @@ export const TableContentSideBar = ({ onClose }: TableContentSideBarProps) => {
9293
`}
9394
>
9495
<Box $direction="row" $align="center" $justify="space-between">
95-
<Text $weight="bold">{t('Table of Contents')}</Text>
96+
<Text as="h2" $weight="bold" $size="16px" $margin="0">
97+
{t('Table of Contents')}
98+
</Text>
9699
<ButtonCloseModal
97100
aria-label={t('Close the table of contents sidebar')}
98101
onClick={onClose}
@@ -138,6 +141,8 @@ export const TableContentSideBarButton = () => {
138141
const { t } = useTranslation();
139142
const { isPanelOpen, activePanel, setActivePanel, setIsPanelOpen } =
140143
useRightPanelStore();
144+
const buttonRef = useRef<ButtonElement>(null);
145+
const { addLastFocus } = useFocusStore();
141146

142147
const isActive = isPanelOpen && activePanel === 'tableContent';
143148
const ariaLabel = isActive
@@ -146,12 +151,14 @@ export const TableContentSideBarButton = () => {
146151

147152
return (
148153
<Button
154+
ref={buttonRef}
149155
size="small"
150156
onClick={() => {
151157
if (isActive) {
152158
setIsPanelOpen(false);
153159
} else {
154160
setActivePanel('tableContent');
161+
addLastFocus(buttonRef.current);
155162
}
156163
}}
157164
aria-label={ariaLabel}

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,22 +41,42 @@ 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}
77+
aria-label={panelLabel}
5778
aria-hidden={!isPanelOpen}
58-
aria-expanded={isPanelOpen}
79+
inert={!isPanelOpen}
5980
$width="300px"
6081
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
6182
$position={isMobile ? 'absolute' : 'sticky'}
@@ -77,6 +98,10 @@ export const RightPanel = () => {
7798
margin-left: 0rem;
7899
width: 0;
79100
`}
101+
102+
&:focus {
103+
outline: none;
104+
}
80105
`}
81106
>
82107
{renderedPanel === 'tableContent' && (

0 commit comments

Comments
 (0)