️(frontend) fix Copy link toast accessibility for screen readers

Add aria-live announcements so screen readers announce the toast feedback.
This commit is contained in:
Cyril
2026-03-17 09:48:59 +01:00
parent 1d819d8fa2
commit 8472e661f5
4 changed files with 18 additions and 31 deletions

View File

@@ -14,6 +14,7 @@ and this project adheres to
- ♿️(frontend) ensure doc title is h1 for accessibility #2006
- ♿️(frontend) fix share modal heading hierarchy #2007
- ♿️(frontend) fix Copy link toast accessibility for screen readers #2029
## [v4.8.1] - 2026-03-17

View File

@@ -46,6 +46,7 @@
"@hocuspocus/provider": "3.4.4",
"@mantine/core": "8.3.14",
"@mantine/hooks": "8.3.14",
"@react-aria/live-announcer": "3.4.4",
"@react-pdf/renderer": "4.3.1",
"@sentry/nextjs": "10.38.0",
"@tanstack/react-query": "5.90.21",

View File

@@ -1,4 +1,5 @@
import { Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
import { announce } from '@react-aria/live-announcer';
import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -80,7 +81,6 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
const [userQuery, setUserQuery] = useState('');
const [inputValue, setInputValue] = useState('');
const [liveAnnouncement, setLiveAnnouncement] = useState('');
const [listHeight, setListHeight] = useState<string>('400px');
const canShare = doc.abilities.accesses_manage && isRootDoc;
@@ -93,18 +93,16 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
setUserQuery('');
setInputValue('');
// Announce to screen readers
const userName = user.full_name || user.email;
setLiveAnnouncement(
announce(
t(
'{{name}} added to invite list. Add more members or press Tab to select role and invite.',
{
name: userName,
},
),
'polite',
);
// Clear announcement after it's been read
setTimeout(() => setLiveAnnouncement(''), 100);
};
const { data: membersQuery } = useDocAccesses({
@@ -132,14 +130,13 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
const newArray = [...prevState];
newArray.splice(index, 1);
// Announce to screen readers
const userName = row.full_name || row.email;
setLiveAnnouncement(
announce(
t('{{name}} removed from invite list', {
name: userName,
}),
'polite',
);
setTimeout(() => setLiveAnnouncement(''), 100);
return newArray;
});
@@ -208,15 +205,6 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
hideCloseButton
>
<ShareModalStyle />
{/* Screen reader announcements */}
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{liveAnnouncement}
</div>
<Box
$height="auto"
$maxHeight={canViewAccesses ? modalContentHeight : 'none'}

View File

@@ -2,6 +2,7 @@ import {
VariantType,
useToastProvider,
} from '@gouvfr-lasuite/cunningham-react';
import { announce } from '@react-aria/live-announcer';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -14,22 +15,18 @@ export const useClipboard = () => {
navigator.clipboard
.writeText(text)
.then(() => {
toast(
successMessage ?? t('Copied to clipboard'),
VariantType.SUCCESS,
{
duration: 3000,
},
);
const message = successMessage ?? t('Copied to clipboard');
toast(message, VariantType.SUCCESS, {
duration: 3000,
});
announce(message, 'polite');
})
.catch(() => {
toast(
errorMessage ?? t('Failed to copy to clipboard'),
VariantType.ERROR,
{
duration: 3000,
},
);
const message = errorMessage ?? t('Failed to copy to clipboard');
toast(message, VariantType.ERROR, {
duration: 3000,
});
announce(message, 'assertive');
});
},
[t, toast],